diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5d21dee60..39fca8c5d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -78,7 +78,7 @@ jobs:
shell: bash
run: |
case ${{ matrix.job.target }} in
- x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev;;
+ x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;;
# arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
# aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
esac
diff --git a/.gitignore b/.gitignore
index 9d152ac1d..da52e1dc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
.vscode
.idea
.DS_Store
+libsciter-gtk.so
src/ui/inline.rs
extractor
__pycache__
diff --git a/Cargo.toml b/Cargo.toml
index 3cbfd47aa..d65c9dde3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,12 +26,13 @@ use_rubato = ["rubato"]
use_dasp = ["dasp"]
flutter = ["flutter_rust_bridge"]
default = ["use_dasp","flutter"]
+hwcodec = ["scrap/hwcodec"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
whoami = "1.2"
-scrap = { path = "libs/scrap" }
+scrap = { path = "libs/scrap", features = ["wayland"] }
hbb_common = { path = "libs/hbb_common" }
serde_derive = "1.0"
serde = "1.0"
@@ -51,10 +52,13 @@ samplerate = { version = "0.2", optional = true }
async-trait = "0.1"
uuid = { version = "1.0", features = ["v4"] }
clap = "3.0"
-rpassword = "6.0"
+rpassword = "7.0"
base64 = "0.13"
-sysinfo = "0.23"
+sysinfo = "0.24"
num_cpus = "1.13"
+bytes = { version = "1.2", features = ["serde"] }
+default-net = "0.11.0"
+wol-rs = "0.9.1"
flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true }
[target.'cfg(not(target_os = "linux"))'.dependencies]
@@ -68,18 +72,18 @@ machine-uid = "0.2"
mac_address = "1.1"
sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" }
sys-locale = "0.2"
-enigo = { path = "libs/enigo" }
+enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" }
rdev = { git = "https://github.com/open-trade/rdev" }
ctrlc = "3.2"
arboard = "2.0"
#minreq = { version = "2.4", features = ["punycode", "https-native"] }
+system_shutdown = "3.0.0"
[target.'cfg(target_os = "windows")'.dependencies]
#systray = { git = "https://github.com/open-trade/systray-rs" }
-trayicon = { version = "0.1", features = ["winit"] }
-# > 0.25 not work with trayicon
-winit = "0.25"
+trayicon = { git = "https://github.com/open-trade/trayicon-rs", features = ["winit"] }
+winit = "0.26"
winapi = { version = "0.3", features = ["winuser"] }
winreg = "0.10"
windows-service = "0.4"
@@ -99,10 +103,12 @@ psimple = { package = "libpulse-simple-binding", version = "2.25" }
pulse = { package = "libpulse-binding", version = "2.26" }
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
async-process = "1.3"
+mouce = { git="https://github.com/fufesou/mouce.git" }
+evdev = { git="https://github.com/fufesou/evdev" }
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11"
-jni = "0.19.0"
+jni = "0.19"
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" }
diff --git a/DEBIAN/postinst b/DEBIAN/postinst
old mode 100644
new mode 100755
index 5899bd4df..1c7697acc
--- a/DEBIAN/postinst
+++ b/DEBIAN/postinst
@@ -8,16 +8,20 @@ if [ "$1" = configure ]; then
if [ "systemd" == "$INITSYS" ]; then
if [ -e /etc/systemd/system/rustdesk.service ]; then
- rm /etc/systemd/system/rustdesk.service
+ rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
fi
version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)')
parsedVersion=$(echo "${version//./}")
if [[ "$parsedVersion" -gt "360" ]]; then
sudo -H pip3 install pynput
fi
- cp /usr/share/rustdesk/files/systemd/rustdesk.service /etc/systemd/system/rustdesk.service
+ cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
systemctl daemon-reload
systemctl enable rustdesk
systemctl start rustdesk
+
+ cp /usr/share/rustdesk/files/systemd/rustdesk.service.user /usr/lib/systemd/user/rustdesk.service
+ curUser=$(who | awk '{print $1}' | head -1)
+ systemctl --machine=${curUser}@.host --user daemon-reload
fi
fi
diff --git a/DEBIAN/postrm b/DEBIAN/postrm
old mode 100644
new mode 100755
diff --git a/DEBIAN/preinst b/DEBIAN/preinst
old mode 100644
new mode 100755
index 8b73e9962..7fbedca4a
--- a/DEBIAN/preinst
+++ b/DEBIAN/preinst
@@ -7,6 +7,13 @@ case $1 in
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
if [ "systemd" == "${INITSYS}" ]; then
service rustdesk stop || true
+
+ serverUser=$(ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1)
+ if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ]
+ then
+ systemctl --machine=${serverUser}@.host --user stop rustdesk || true
+ fi
+
sleep 1
rm -rf /usr/bin/libsciter-gtk.so
fi
diff --git a/DEBIAN/prerm b/DEBIAN/prerm
old mode 100644
new mode 100755
index 865b689ab..3bb453198
--- a/DEBIAN/prerm
+++ b/DEBIAN/prerm
@@ -8,7 +8,14 @@ case $1 in
if [ "systemd" == "${INITSYS}" ]; then
systemctl stop rustdesk || true
systemctl disable rustdesk || true
- rm /etc/systemd/system/rustdesk.service || true
+
+ serverUser=$(ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1)
+ if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ]
+ then
+ systemctl --machine=${serverUser}@.host --user stop rustdesk || true
+ fi
+
+ rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service || true
fi
;;
esac
diff --git a/README-AR.md b/README-AR.md
index 055a654d2..2deb4914b 100644
--- a/README-AR.md
+++ b/README-AR.md
@@ -5,7 +5,7 @@
Docker •
Structure •
Snapshot
- [česky] | [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
لغتك الأم, Doc و RustDesk UI, README نحن بحاجة إلى مساعدتك لترجمة هذا
diff --git a/README-CS.md b/README-CS.md
index 1dd5463a1..f6fa2fbf0 100644
--- a/README-CS.md
+++ b/README-CS.md
@@ -5,7 +5,7 @@
Docker •
Struktura •
Ukázky
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Potřebujeme Vaši pomoc s překláním textů tohoto ČTIMNE, uživatelského rozhraní aplikace RustDesk a dokumentace k ní do vašeho jazyka
diff --git a/README-DE.md b/README-DE.md
index 3f770d226..4e9929997 100644
--- a/README-DE.md
+++ b/README-DE.md
@@ -5,11 +5,11 @@
Docker •
Dateistruktur •
Screenshots
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren
-Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
diff --git a/README-EO.md b/README-EO.md
index 21a4f9521..b9af26102 100644
--- a/README-EO.md
+++ b/README-EO.md
@@ -5,11 +5,11 @@
Docker •
Strukturo •
Ekrankopio
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Ni bezonas helpon traduki tiun README kaj la interfacon al via denaska lingvo
-Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
diff --git a/README-ES.md b/README-ES.md
index ce8601fa0..1aab59213 100644
--- a/README-ES.md
+++ b/README-ES.md
@@ -5,7 +5,7 @@
Docker •
Estructura •
Captura de pantalla
- [česky] | [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Necesitamos tu ayuda para traducir este README a tu idioma
diff --git a/README-FA.md b/README-FA.md
index 0f7ca1a95..818e62fa8 100644
--- a/README-FA.md
+++ b/README-FA.md
@@ -5,11 +5,11 @@
داکر •
ساخت •
سرور
- [česky] | [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
برای ترجمه این RustDesk UI ،README و Doc به زبان مادری شما به کمکتون نیاز داریم
-با ما گپ بزنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV)
+با ما گپ بزنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
diff --git a/README-FI.md b/README-FI.md
index a2d7534e0..1258bb550 100644
--- a/README-FI.md
+++ b/README-FI.md
@@ -5,11 +5,11 @@
Docker •
Rakenne •
Tilannevedos
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi
-Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
diff --git a/README-FR.md b/README-FR.md
index b1f8e3670..9a303e6e6 100644
--- a/README-FR.md
+++ b/README-FR.md
@@ -5,11 +5,11 @@
Docker -
Structure -
Images
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle.
-Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
diff --git a/README-HU.md b/README-HU.md
new file mode 100644
index 000000000..7055ed446
--- /dev/null
+++ b/README-HU.md
@@ -0,0 +1,182 @@
+
+
+ Szerverek •
+ Építés •
+ Docker •
+ Struktúra •
+ Képernyőképek
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ Kell a segítséged, hogy lefordítsuk ezt a README-t, a RustDesk UI-t és a Dokumentációt az anyanyelvedre
+
+
+Beszélgess velünk: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+
+[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
+
+A RustDesk egy távoli elérésű asztali szoftver, Rust-ban írva. Működik mindenféle konfiguráció nélkül, feltelepítéssel, vagy anélkül. Az adataidat teljesen te kezeled, nincs szükség aggódásra a harmadik felek miatt. Használhatod a RustDesk punblikus randevú/relay szervereit, [hostolhatsz sajátot](https://rustdesk.com/server), vagy akár [írhatsz is egyet](https://github.com/rustdesk/rustdesk-server-demo).
+
+![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
+
+A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lásd a [`CONTRIBUTING.md`](CONTRIBUTING.md) fájlt a kezdéshez.
+
+[**Hogyan működik a RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
+
+[**BINARY LELTÖLTÉS**](https://github.com/rustdesk/rustdesk/releases)
+
+[](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+
+## Ingyenes publikus szerverek
+
+Ezalatt az üzenet alatt találhatóak azok a publikus szerverek, amelyeket ingyen használhatsz. Ezek a szerverek változhatnak a jövőben, illetve a hálózatuk lehet hogy lassú lehet.
+| Hely | Host | Specifikáció |
+| --------- | ------------- | ------------------ |
+| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
+| Singapore | Vultr | 1 VCPU / 1GB RAM |
+| Dallas | Vultr | 1 VCPU / 1GB RAM | |
+
+## Dependencies
+
+Az asztali verziók [sciter](https://sciter.com/)-t használnak a GUI-hoz, kérlek telepítsd a dynamikus könyvtárat magad.
+
+[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
+[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
+[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
+
+A telefonos verziók Flutter-t hasznának. Később lehetséges hogy Sciterről Flutterre migrálunk az asztali verziókban is.
+
+## Építési pontok
+
+- Készítsd elő a Rust, C++ fejlesztői környezetet (env)
+
+- Telepítsd a [vcpkg](https://github.com/microsoft/vcpkg)-t, és állítsd be a `VCPKG_ROOT` környezeti változót helyesen
+
+ - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
+ - Linux/MacOS: vcpkg install libvpx libyuv opus
+
+- Futtasd a `cargo run` parancsot
+
+## [Építés](https://rustdesk.com/docs/hu/dev/build/)
+
+## Hogyan építs Linuxon
+
+### Ubuntu 18 (Debian 10)
+
+```sh
+sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
+```
+
+### Fedora 28 (CentOS 8)
+
+```sh
+sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
+```
+
+### Arch (Manjaro)
+
+```sh
+sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
+```
+
+### Telepítsd a pynput csomagot
+
+```sh
+pip3 install pynput
+```
+
+### Telepítsd a vcpkg-t
+
+```sh
+git clone https://github.com/microsoft/vcpkg
+cd vcpkg
+git checkout 2021.12.01
+cd ..
+vcpkg/bootstrap-vcpkg.sh
+export VCPKG_ROOT=$HOME/vcpkg
+vcpkg/vcpkg install libvpx libyuv opus
+```
+
+### Fixeld a libvpx-t (Fedora-n csak)
+
+```sh
+cd vcpkg/buildtrees/libvpx/src
+cd *
+./configure
+sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
+sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
+make
+cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
+cd
+```
+
+### Építés
+
+```sh
+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+source $HOME/.cargo/env
+git clone https://github.com/rustdesk/rustdesk
+cd rustdesk
+mkdir -p target/debug
+wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
+mv libsciter-gtk.so target/debug
+VCPKG_ROOT=$HOME/vcpkg cargo run
+```
+
+### Válts Wayland-ról X11-re (Xorg)
+
+A RustDesk nem támogatja a Waylendet. [Itt](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) található egy tutorial amelynek segítségével beállíthatod a Xorg-ot mint alap GNOME session.
+
+## Hogyan építs Dockerrel
+
+Kezdjünk a repo clónozásával, majd pedig a Docker container megépítésével:
+
+```sh
+git clone https://github.com/rustdesk/rustdesk
+cd rustdesk
+docker build -t "rustdesk-builder" .
+```
+
+Ezután, minden egyes alkalommal amikor meg kell építened a RustDesk-et, futtasd a kövezkező parancsot:
+
+```sh
+docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
+```
+
+Fontos, hogy az első építés lehet hogy több ideig fog tartani mint a következőek, mivel a dependenciek még nincsenek cachelve. Emelett, ha esetleg szeretnél valamilyen argumentumot hozzáadni az építő parancshoz, akkor megteheted a paracssor végén, a `` argumentum használatával. Például ha egy optimalizált release éptést szeretnél megépíteni, akkor add hozzá a fenti parancsorhoz a `--release` opciót. A futtatható binary elérhető lesz a target mappában a rendszereden, futtatni a következőképpen tudod:
+
+```sh
+target/debug/rustdesk
+```
+
+Vagy ha release binary, akkor:
+
+```sh
+target/release/rustdesk
+```
+
+Kérlek mindenképpen nézd meg hogy ezeket a parancsokat a root RustDesk mappában futtatod e, különben a RustDesk lehet hogy nem fogja megtalálni az építéshez szükséges elemeket. Fontos az is, hogy jelenleg más cargo subparancsok, például `install`vagy `run` nem támogatottak, mivel egy Dockeres építés esetén elindítanák a programot a containeren belül.
+
+
+## Fájl Struktúra
+
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions
+- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture
+- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control
+- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
+- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections
+- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
+- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
+
+## Képernyőképek
+
+![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
+
+![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png)
+
+![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
+
+![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)
diff --git a/README-ID.md b/README-ID.md
index 624336f45..363d4263b 100644
--- a/README-ID.md
+++ b/README-ID.md
@@ -5,11 +5,11 @@
Docker •
Structure •
Snapshot
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Kami membutuhkan bantuan Anda untuk menerjemahkan README ini dan RustDesk UI ke bahasa asli anda
-Birbincang bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+Birbincang bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
diff --git a/README-IT.md b/README-IT.md
index 7eba7860a..a3f36af55 100644
--- a/README-IT.md
+++ b/README-IT.md
@@ -5,11 +5,11 @@
Docker •
Struttura •
Screenshots
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Abbiamo bisogno del tuo aiuto per tradurre questo README e la RustDesk UI nella tua lingua nativa
-Chatta con noi: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+Chatta con noi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
diff --git a/README-JP.md b/README-JP.md
index 60816a5d5..fb55d0ced 100644
--- a/README-JP.md
+++ b/README-JP.md
@@ -5,7 +5,7 @@
Docker •
Structure •
Snapshot
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。
diff --git a/README-KR.md b/README-KR.md
index 750cf91bd..00564e298 100644
--- a/README-KR.md
+++ b/README-KR.md
@@ -5,7 +5,7 @@
Docker •
Structure •
Snapshot
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.
diff --git a/README-ML.md b/README-ML.md
index c479d0496..d2931a2c7 100644
--- a/README-ML.md
+++ b/README-ML.md
@@ -5,11 +5,11 @@
Docker •
Structure •
Snapshot
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്
-ഞങ്ങളുമായി ചാറ്റ് ചെയ്യുക: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+ഞങ്ങളുമായി ചാറ്റ് ചെയ്യുക: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
diff --git a/README-NL.md b/README-NL.md
index 2d87504db..5db299e7c 100644
--- a/README-NL.md
+++ b/README-NL.md
@@ -5,11 +5,11 @@
Docker •
Structuur •
Snapshot
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
We hebben je hulp nodig om deze README te vertalen naar jouw moedertaal
-Praat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+Praat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
diff --git a/README-PL.md b/README-PL.md
index 162ca7648..119af95cf 100644
--- a/README-PL.md
+++ b/README-PL.md
@@ -5,11 +5,11 @@
Docker •
Struktura •
Snapshot
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język
-Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
diff --git a/README-PTBR.md b/README-PTBR.md
index 76b360283..955456256 100644
--- a/README-PTBR.md
+++ b/README-PTBR.md
@@ -5,11 +5,11 @@
Docker •
Estrutura •
Screenshots
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Precisamos de sua ajuda para traduzir este README e a UI do RustDesk para sua língua nativa
-Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
+Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
diff --git a/README-RU.md b/README-RU.md
index 54c161cf0..a9d81152c 100644
--- a/README-RU.md
+++ b/README-RU.md
@@ -1,11 +1,11 @@
-
+
Servers •
Build •
Docker •
Structure •
Snapshot
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Нам нужна ваша помощь для перевода этого README и RustDesk UI на ваш родной язык
@@ -15,10 +15,16 @@
Еще одно программное обеспечение для удаленного рабочего стола, написанное на Rust. Работает из коробки, не требует настройки. Вы полностью контролируете свои данные, не беспокоясь о безопасности. Вы можете использовать наш сервер ретрансляции, [настроить свой собственный](https://rustdesk.com/server), или [написать свой собственный сервер ретрансляции](https://github.com/rustdesk/rustdesk-server-demo).
+![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
+
RustDesk приветствует вклад каждого. Смотрите [`CONTRIBUTING.md`](CONTRIBUTING.md) для помощи в начале работы.
+[**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
+
[**СКАЧАТЬ ПРИЛОЖЕНИЕ**](https://github.com/rustdesk/rustdesk/releases)
+[](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+
## Бесплатные общедоступные серверы
Ниже приведены серверы, для бесплатного использования, они могут меняться со временем. Если вы не находитесь рядом с одним из них, ваша сеть может работать медленно.
@@ -81,7 +87,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus
```
-### Исправление libvpx (Для Fedora)
+### Исправление libvpx (для Fedora)
```sh
cd vcpkg/buildtrees/libvpx/src
diff --git a/README-VN.md b/README-VN.md
new file mode 100644
index 000000000..b39005a31
--- /dev/null
+++ b/README-VN.md
@@ -0,0 +1,182 @@
+
+
+ Máy chủ •
+ Build •
+ Docker •
+ Cấu trúc tệp tin •
+ Snapshot
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, RustDesk UI và tài liệu sang ngôn ngữ bản địa của bạn
+
+
+Chat với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+
+[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
+
+Một phần mềm điểu khiển máy tính từ xa, đuợc lập trình bằng ngôn ngữ Rust. Hoạt động tức thì, không cần phải cài đặt. Bạn có toàn quyền điểu khiển với dữ liệu của bạn mà không cần phải lo lắng về sự bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi, [tự cài đặt máy chủ](https://rustdesk.com/server), hay thậm chí [tự tạo máy chủ rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
+
+![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
+
+Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`CONTRIBUTING.md`](CONTRIBUTING.md).
+
+[**RustDesk hoạt động như thế nào?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
+
+[**CÁC BẢN PHÂN PHÁT MÃ NHỊ PHÂN**](https://github.com/rustdesk/rustdesk/releases)
+
+[](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+
+## Các Máy Chủ Công Khai Miễn Phí
+
+Dưới đây là những máy chủ mà bạn có thể sử dụng mà không mất phí, chú ý là máy chủ có thể thay đổi theo thời gian. Nếu địa điểm của bạn không gần một trong số những máy chủ này, thì kết nói có thể chậm.
+
+| Địa điểm | Nhà cung cấp | Cấu hình |
+| --------- | ------------- | ------------------ |
+| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
+| Singapore | Vultr | 1 VCPU / 1GB RAM |
+| Dallas | Vultr | 1 VCPU / 1GB RAM | |
+
+## Dependencies
+
+Phiên bản cho máy tính sử dụng [sciter](https://sciter.com/) cho giao diện của phần mềm, vậy nên bạn cần tự tải về thư viện sciter.
+
+[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
+[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
+[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
+
+Phiên bản cho điện thoại sử dụng Flutter. Chúng tôi sẽ chuyển sang sử dụng Flutter thay cho Sciter cho phiên bản máy tính.
+
+## Cách để build
+
+- Chuẩn bị môi trường phát triển Rust và môi trường build C++
+
+- Tải và cài [vcpkg](https://github.com/microsoft/vcpkg), và đặt biến môi trường `VCPKG_ROOT` sao cho đúng.
+
+ - Đối với Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
+ - Đối với Linux/MacOS: vcpkg install libvpx libyuv opus
+
+- Chạy lệnh `cargo run`
+
+## [Build](https://rustdesk.com/docs/en/dev/build/)
+
+## Cách để build cho Linux
+
+### Ubuntu 18 (Debian 10)
+
+```sh
+sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
+```
+
+### Fedora 28 (CentOS 8)
+
+```sh
+sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
+```
+
+### Arch (Manjaro)
+
+```sh
+sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
+```
+
+### Cách tải về gói hàng pynput
+
+```sh
+pip3 install pynput
+```
+
+### Cách cài vcpkg
+
+```sh
+git clone https://github.com/microsoft/vcpkg
+cd vcpkg
+git checkout 2021.12.01
+cd ..
+vcpkg/bootstrap-vcpkg.sh
+export VCPKG_ROOT=$HOME/vcpkg
+vcpkg/vcpkg install libvpx libyuv opus
+```
+
+### Cách sửa lỗi libvpx (Dành cho hệ điều hành Fedora)
+
+```sh
+cd vcpkg/buildtrees/libvpx/src
+cd *
+./configure
+sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
+sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
+make
+cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
+cd
+```
+
+### Cách build
+
+```sh
+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+source $HOME/.cargo/env
+git clone https://github.com/rustdesk/rustdesk
+cd rustdesk
+mkdir -p target/debug
+wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
+mv libsciter-gtk.so target/debug
+VCPKG_ROOT=$HOME/vcpkg cargo run
+```
+
+### Chuyển từ Wayland sang X11 (Xorg)
+
+RustDesk hiện không hỗ trợ Wayland. Hãy xem [đường linh ở đây](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) cách để cài đặt Xorg làm session mặc định của GNOME.
+
+## Cách để build sử dụng Docker
+
+Bắt đầu bằng cách sao chép repo này về máy tính và build cái Docker cointainer:
+
+```sh
+git clone https://github.com/rustdesk/rustdesk
+cd rustdesk
+docker build -t "rustdesk-builder" .
+```
+
+Rồi mỗi khi bạn chạy ứng dụng, thì hãy chạy lệnh này:
+
+```sh
+docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
+```
+
+Chú ý: Lần build đầu tiên có thể sẽ mất lâu hơn truớc khi các dependecies đuợc lưu lại, những lần build sau sẽ nhanh hơn. Hơn nũa, nếu bạn cần cung cấp các cài đặt lệnh khác cho lệnh build, bạn có thể đặt những cài đặt lệnh này vào cuối lệnh ở phần ``. Ví dụ nếu bạn cần build phiên bản đuợc tối ưu hóa, bạn sẽ chạy lệnh trên cùng với cài đặt lệnh ‘--release’. Kết quả build sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
+
+```sh
+target/debug/rustdesk
+```
+
+Nếu bạn đang chạy bản build đuợc tối ưu hóa, thì bạn có thể chạy với lệnh:
+
+```sh
+target/release/rustdesk
+```
+
+Hãy đảm bảo là bạn đang chạy những lệnh này từ thu mục rễ của repo RustDesk, vì nếu không thì ứng dụng có thể sẽ không tìm đuợc những tệp tài nguyên cần thiết. Cũng như nhớ rằng những lệnh con của cargo như `install` hoặc `run` hiện chưa được hỗ trợ bởi phương pháp này vì chúng sẽ cài đặt hoặc chạy ứng dụng trong container thay vì trên máy tính của bạn.
+
+## Cấu trúc tệp tin
+
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác
+- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: để ghi lại màn hình
+- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: để điều khiển máy tính/con chuột trên những nền tảng khác nhau
+- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng
+- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng
+- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: để bắt đầu kết nối với một peer
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Để liên lạc với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi cho kết nối trực tiếp (TCP hole punching) hoặc kết nối được relayed.
+- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành cho điện thoại
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter
+
+## Snapshot
+
+![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
+
+![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png)
+
+![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
+
+![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)
diff --git a/README-ZH.md b/README-ZH.md
index 8d4203b16..0c3e7d5c1 100644
--- a/README-ZH.md
+++ b/README-ZH.md
@@ -5,7 +5,7 @@
Docker •
结构 •
截图
- [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
@@ -202,7 +202,7 @@ target/release/rustdesk
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 截屏
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
-- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务,audio/clipboard/input/video 服务, 已经连接实现
+- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务,audio/clipboard/input/video 服务, 以及连接的实现
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持 UDP 通讯, 等待远程连接(通过打洞直连或者中继)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
diff --git a/README.md b/README.md
index 2166073a7..346600f61 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
Docker •
Structure •
Snapshot
- [česky] | [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
We need your help to translate this README, RustDesk UI and Doc to your native language
@@ -155,7 +155,7 @@ Or, if you're running a release executable:
target/release/rustdesk
```
-Please ensure that you are running these commands from the root of the RustDesk repository, otherwise the application may be unable to find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host.
+Please ensure that you are running these commands from the root of the RustDesk repository, otherwise the application might not be able to find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host.
## File Structure
diff --git a/build.py b/build.py
old mode 100644
new mode 100755
index 45488ad63..341f4f4e6
--- a/build.py
+++ b/build.py
@@ -22,7 +22,7 @@ def get_version():
return ''
-def get_features(feature):
+def parse_rc_features(feature):
available_features = {
'IddDriver': {
'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64.zip',
@@ -66,6 +66,11 @@ def make_parser():
default='',
help='Integrate features, windows only.'
'Available: IddDriver, PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
+ parser.add_argument(
+ '--hwcodec',
+ action='store_true',
+ help='Enable feature hwcodec, windows only.'
+ )
return parser
@@ -89,11 +94,9 @@ def download_extract_features(features, res_dir):
print(f'{feat} extract end')
-def build_windows(feature):
- features = get_features(feature)
- if not features:
- os.system('cargo build --release --features inline')
- else:
+def get_rc_features(args):
+ features = parse_rc_features(args.feature)
+ if features:
print(f'Build with features {list(features.keys())}')
res_dir = 'resources'
if os.path.isdir(res_dir) and not os.path.islink(res_dir):
@@ -102,8 +105,17 @@ def build_windows(feature):
raise Exception(f'Find file {res_dir}, not a directory')
os.makedirs(res_dir, exist_ok=True)
download_extract_features(features, res_dir)
- os.system('cargo build --release --features inline,with_rc')
+ return ['with_rc'] if features else []
+
+def get_features(args):
+ features = ['inline']
+ if windows:
+ features.extend(get_rc_features(args))
+ if args.hwcodec:
+ features.append('hwcodec')
+ print("features:", features)
+ return features
def main():
parser = make_parser()
@@ -122,8 +134,9 @@ def main():
if os.path.isfile('/usr/bin/pacman'):
os.system('git checkout src/ui/common.tis')
version = get_version()
+ features = ",".join(get_features(args))
if windows:
- build_windows(args.feature)
+ os.system('cargo build --release --features ' + features)
# os.system('upx.exe target/release/rustdesk.exe')
os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe')
pa = os.environ.get('P')
@@ -134,7 +147,7 @@ def main():
print('Not signed')
os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe')
elif os.path.isfile('/usr/bin/pacman'):
- os.system('cargo build --release --features inline')
+ os.system('cargo build --release --features ' + features)
os.system('git checkout src/ui/common.tis')
os.system('strip target/release/rustdesk')
os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version)
@@ -143,7 +156,7 @@ def main():
os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version))
# pacman -U ./rustdesk.pkg.tar.zst
elif os.path.isfile('/usr/bin/yum'):
- os.system('cargo build --release --features inline')
+ os.system('cargo build --release --features ' + features)
os.system('strip target/release/rustdesk')
os.system("sed -i 's/Version: .*/Version: %s/g' rpm.spec" % version)
os.system('HBB=`pwd` rpmbuild -ba rpm.spec')
@@ -151,14 +164,14 @@ def main():
version, version))
# yum localinstall rustdesk.rpm
elif os.path.isfile('/usr/bin/zypper'):
- os.system('cargo build --release --features inline')
+ os.system('cargo build --release --features ' + features)
os.system('strip target/release/rustdesk')
os.system("sed -i 's/Version: .*/Version: %s/g' rpm-suse.spec" % version)
os.system('HBB=`pwd` rpmbuild -ba rpm-suse.spec')
os.system('mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % (version, version))
# yum localinstall rustdesk.rpm
else:
- os.system('cargo bundle --release --features inline')
+ os.system('cargo bundle --release --features ' + features)
if osx:
os.system(
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk')
@@ -202,12 +215,15 @@ rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9
os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
os.system(
'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
+ os.system(
+ 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/')
os.system('cp pynput_service.py tmpdeb/usr/share/rustdesk/files/')
- os.system('cp DEBIAN/* tmpdeb/DEBIAN/')
+ os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/')
os.system('strip tmpdeb/usr/bin/rustdesk')
os.system('mkdir -p tmpdeb/usr/lib/rustdesk')
os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
+ md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user')
md5_file('usr/share/rustdesk/files/pynput_service.py')
md5_file('usr/lib/rustdesk/libsciter-gtk.so')
os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml
index ad496edb0..04b2ccc9a 100644
--- a/flutter/android/app/src/main/AndroidManifest.xml
+++ b/flutter/android/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
package="com.carriez.flutter_hbb">
+
@@ -43,7 +44,7 @@
()
private var isWheelActionsPolling = false
+ private var isWaitingLongPress = false
@RequiresApi(Build.VERSION_CODES.N)
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
- val x = if (_x < 0) {
- 0
- } else {
- _x
- }
-
- val y = if (_y < 0) {
- 0
- } else {
- _y
- }
+ val x = max(0, _x)
+ val y = max(0, _y)
if (mask == 0 || mask == LIFT_MOVE) {
+ val oldX = mouseX
+ val oldY = mouseY
mouseX = x * SCREEN_INFO.scale
mouseY = y * SCREEN_INFO.scale
+ if (isWaitingLongPress) {
+ val delta = abs(oldX - mouseX) + abs(oldY - mouseY)
+ Log.d(logTag,"delta:$delta")
+ if (delta > 8) {
+ isWaitingLongPress = false
+ }
+ }
}
// left button down ,was up
if (mask == LIFT_DOWN) {
+ isWaitingLongPress = true
+ timer.schedule(object : TimerTask() {
+ override fun run() {
+ if (isWaitingLongPress) {
+ isWaitingLongPress = false
+ leftIsDown = false
+ endGesture(mouseX, mouseY)
+ }
+ }
+ }, LONG_TAP_DELAY * 4)
+
leftIsDown = true
startGesture(mouseX, mouseY)
return
@@ -83,9 +95,12 @@ class InputService : AccessibilityService() {
// left up ,was down
if (mask == LIFT_UP) {
- leftIsDown = false
- endGesture(mouseX, mouseY)
- return
+ if (leftIsDown) {
+ leftIsDown = false
+ isWaitingLongPress = false
+ endGesture(mouseX, mouseY)
+ return
+ }
}
if (mask == RIGHT_UP) {
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
index 3cc105bfa..fd340f7ed 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
@@ -192,7 +192,6 @@ class MainActivity : FlutterActivity() {
override fun onResume() {
super.onResume()
val inputPer = InputService.isOpen
- Log.d(logTag, "onResume inputPer:$inputPer")
activity.runOnUiThread {
flutterMethodChannel.invokeMethod(
"on_state_changed",
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt
index 7ce7d3ecc..4bf244a06 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt
@@ -2,20 +2,26 @@ package com.carriez.flutter_hbb
import android.annotation.SuppressLint
import android.content.Context
+import android.content.Intent
import android.media.AudioRecord
import android.media.AudioRecord.READ_BLOCKING
import android.media.MediaCodecList
import android.media.MediaFormat
+import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
-import android.util.Log
+import android.os.PowerManager
+import android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
+import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
import androidx.annotation.RequiresApi
+import androidx.core.content.ContextCompat.getSystemService
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import java.nio.ByteBuffer
import java.util.*
+
@SuppressLint("ConstantLocale")
val LOCAL_NAME = Locale.getDefault().toString()
val SCREEN_INFO = Info(0, 0, 1, 200)
@@ -38,8 +44,31 @@ fun testVP9Support(): Boolean {
return res != null
}
+@RequiresApi(Build.VERSION_CODES.M)
fun requestPermission(context: Context, type: String) {
val permission = when (type) {
+ "ignore_battery_optimizations" -> {
+ try {
+ context.startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
+ data = Uri.parse("package:" + context.packageName)
+ })
+ } catch (e:Exception) {
+ e.printStackTrace()
+ }
+ return
+ }
+ "application_details_settings" -> {
+ try {
+ context.startActivity(Intent().apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ action = "android.settings.APPLICATION_DETAILS_SETTINGS"
+ data = Uri.parse("package:" + context.packageName)
+ })
+ } catch (e:Exception) {
+ e.printStackTrace()
+ }
+ return
+ }
"audio" -> {
Permission.RECORD_AUDIO
}
@@ -52,7 +81,7 @@ fun requestPermission(context: Context, type: String) {
}
XXPermissions.with(context)
.permission(permission)
- .request { permissions, all ->
+ .request { _, all ->
if (all) {
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
@@ -64,8 +93,13 @@ fun requestPermission(context: Context, type: String) {
}
}
+@RequiresApi(Build.VERSION_CODES.M)
fun checkPermission(context: Context, type: String): Boolean {
val permission = when (type) {
+ "ignore_battery_optimizations" -> {
+ val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+ return pw.isIgnoringBatteryOptimizations(context.packageName)
+ }
"audio" -> {
Permission.RECORD_AUDIO
}
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index 47a663768..eda1ed4e7 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -282,7 +282,12 @@ class PermissionManager {
static Timer? _timer;
static var _current = "";
- static final permissions = ["audio", "file"];
+ static final permissions = [
+ "audio",
+ "file",
+ "ignore_battery_optimizations",
+ "application_details_settings"
+ ];
static bool isWaitingFile() {
if (_completer != null) {
@@ -301,6 +306,10 @@ class PermissionManager {
if (!permissions.contains(type))
return Future.error("Wrong permission!$type");
+ gFFI.invokeMethod("request_permission", type);
+ if (type == "ignore_battery_optimizations") {
+ return Future.value(false);
+ }
_current = type;
_completer = Completer();
gFFI.invokeMethod("request_permission", type);
@@ -328,6 +337,18 @@ class PermissionManager {
}
}
+RadioListTile getRadio(
+ String name, T toValue, T curValue, void Function(T?) onChange) {
+ return RadioListTile(
+ controlAffinity: ListTileControlAffinity.trailing,
+ title: Text(translate(name)),
+ value: toValue,
+ groupValue: curValue,
+ onChanged: onChange,
+ dense: true,
+ );
+}
+
/// find ffi, tag is Remote ID
/// for session specific usage
FFI ffi(String? tag) {
diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart
index a0ea0f17c..23900ef07 100644
--- a/flutter/lib/mobile/pages/remote_page.dart
+++ b/flutter/lib/mobile/pages/remote_page.dart
@@ -264,7 +264,6 @@ class _RemotePageState extends State {
: SafeArea(child:
OrientationBuilder(builder: (ctx, orientation) {
if (_currentOrientation != orientation) {
- debugPrint("on orientation changed");
Timer(Duration(milliseconds: 200), () {
resetMobileActionsOverlay();
_currentOrientation = orientation;
@@ -345,9 +344,14 @@ class _RemotePageState extends State {
onKey: (data, e) {
final key = e.logicalKey;
if (e is RawKeyDownEvent) {
- if (e.repeat) {
+ if (e.repeat &&
+ !e.isAltPressed &&
+ !e.isControlPressed &&
+ !e.isShiftPressed &&
+ !e.isMetaPressed) {
sendRawKey(e, press: true);
} else {
+ sendRawKey(e, down: true);
if (e.isAltPressed && !gFFI.alt) {
gFFI.alt = true;
} else if (e.isControlPressed && !gFFI.ctrl) {
@@ -357,7 +361,6 @@ class _RemotePageState extends State {
} else if (e.isMetaPressed && !gFFI.command) {
gFFI.command = true;
}
- sendRawKey(e, down: true);
}
}
// [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter
@@ -483,6 +486,7 @@ class _RemotePageState extends State {
/// DoubleFiner -> right click
/// HoldDrag -> left drag
+ Offset _cacheLongPressPosition = Offset(0, 0);
Widget getBodyForMobileWithGesture() {
final touchMode = gFFI.ffiModel.touchMode;
return getMixinGestureDetector(
@@ -507,9 +511,14 @@ class _RemotePageState extends State {
onLongPressDown: (d) {
if (touchMode) {
gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+ _cacheLongPressPosition = d.localPosition;
}
},
onLongPress: () {
+ if (touchMode) {
+ gFFI.cursorModel
+ .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
+ }
gFFI.tap(MouseButtons.right);
},
onDoubleFinerTap: (d) {
@@ -536,6 +545,15 @@ class _RemotePageState extends State {
if (touchMode) {
gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
gFFI.sendMouse('down', MouseButtons.left);
+ } else {
+ final cursorX = gFFI.cursorModel.x;
+ final cursorY = gFFI.cursorModel.y;
+ final visible =
+ gFFI.cursorModel.getVisibleRect().inflate(1); // extend edges
+ final size = MediaQueryData.fromWindow(ui.window).size;
+ if (!visible.contains(Offset(cursorX, cursorY))) {
+ gFFI.cursorModel.move(size.width / 2, size.height / 2);
+ }
}
},
onOneFingerPanUpdate: (d) {
@@ -946,18 +964,6 @@ CheckboxListTile getToggle(void Function(void Function()) setState, option, name
title: Text(translate(name)));
}
-RadioListTile getRadio(String name, String toValue, String curValue,
- void Function(String?) onChange) {
- return RadioListTile(
- controlAffinity: ListTileControlAffinity.trailing,
- title: Text(translate(name)),
- value: toValue,
- groupValue: curValue,
- onChanged: onChange,
- dense: true,
- );
-}
-
void showOptions() {
String quality = gFFI.getByName('image_quality');
if (quality == '') quality = 'balanced';
@@ -1045,6 +1051,8 @@ void showOptions() {
getRadio('Optimize reaction time', 'low', quality, setQuality),
Divider(color: MyTheme.border),
getToggle(setState, 'show-remote-cursor', 'Show remote cursor'),
+ getToggle(
+ setState, 'show-quality-monitor', 'Show quality monitor'),
] +
more),
actions: [],
diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart
index 9adddf280..b5a6dd1c9 100644
--- a/flutter/lib/mobile/pages/server_page.dart
+++ b/flutter/lib/mobile/pages/server_page.dart
@@ -1,3 +1,5 @@
+import 'dart:convert';
+
import 'package:flutter/material.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:flutter_hbb/models/model.dart';
@@ -24,36 +26,84 @@ class ServerPage extends StatelessWidget implements PageShape {
return [
PopupMenuItem(
child: Text(translate("Change ID")),
+ padding: EdgeInsets.symmetric(horizontal: 16.0),
value: "changeID",
enabled: false,
),
PopupMenuItem(
- child: Text(translate("Set your own password")),
- value: "changePW",
- enabled: gFFI.serverModel.isStart,
+ child: Text(translate("Set permanent password")),
+ padding: EdgeInsets.symmetric(horizontal: 16.0),
+ value: "setPermanentPassword",
+ enabled:
+ gFFI.serverModel.verificationMethod != kUseTemporaryPassword,
),
PopupMenuItem(
- child: Text(translate("Refresh random password")),
- value: "refreshPW",
- enabled: gFFI.serverModel.isStart,
- )
+ child: Text(translate("Set temporary password length")),
+ padding: EdgeInsets.symmetric(horizontal: 16.0),
+ value: "setTemporaryPasswordLength",
+ enabled:
+ gFFI.serverModel.verificationMethod != kUsePermanentPassword,
+ ),
+ const PopupMenuDivider(),
+ PopupMenuItem(
+ padding: EdgeInsets.symmetric(horizontal: 0.0),
+ value: kUseTemporaryPassword,
+ child: Container(
+ child: ListTile(
+ title: Text(translate("Use temporary password")),
+ trailing: Icon(
+ Icons.check,
+ color: gFFI.serverModel.verificationMethod ==
+ kUseTemporaryPassword
+ ? null
+ : Color(0xFFFFFFFF),
+ ))),
+ ),
+ PopupMenuItem(
+ padding: EdgeInsets.symmetric(horizontal: 0.0),
+ value: kUsePermanentPassword,
+ child: ListTile(
+ title: Text(translate("Use permanent password")),
+ trailing: Icon(
+ Icons.check,
+ color: gFFI.serverModel.verificationMethod ==
+ kUsePermanentPassword
+ ? null
+ : Color(0xFFFFFFFF),
+ )),
+ ),
+ PopupMenuItem(
+ padding: EdgeInsets.symmetric(horizontal: 0.0),
+ value: kUseBothPasswords,
+ child: ListTile(
+ title: Text(translate("Use both passwords")),
+ trailing: Icon(
+ Icons.check,
+ color: gFFI.serverModel.verificationMethod !=
+ kUseTemporaryPassword &&
+ gFFI.serverModel.verificationMethod !=
+ kUsePermanentPassword
+ ? null
+ : Color(0xFFFFFFFF),
+ )),
+ ),
];
},
onSelected: (value) {
if (value == "changeID") {
// TODO
- } else if (value == "changePW") {
- updatePasswordDialog();
- } else if (value == "refreshPW") {
- () async {
- showLoading(translate("Waiting"));
- if (await gFFI.serverModel.updatePassword("")) {
- showSuccess();
- } else {
- showError();
- }
- debugPrint("end updatePassword");
- }();
+ } else if (value == "setPermanentPassword") {
+ setPermanentPasswordDialog();
+ } else if (value == "setTemporaryPasswordLength") {
+ setTemporaryPasswordLengthDialog();
+ } else if (value == kUsePermanentPassword ||
+ value == kUseTemporaryPassword ||
+ value == kUseBothPasswords) {
+ Map msg = Map()
+ ..["name"] = "verification-method"
+ ..["value"] = value;
+ gFFI.setByName('option', jsonEncode(msg));
+ gFFI.serverModel.updatePasswordModel();
}
})
];
@@ -90,17 +140,13 @@ void checkService() async {
}
}
-class ServerInfo extends StatefulWidget {
- @override
- _ServerInfoState createState() => _ServerInfoState();
-}
-
-class _ServerInfoState extends State {
+class ServerInfo extends StatelessWidget {
final model = gFFI.serverModel;
- var _passwdShow = false;
+ final emptyController = TextEditingController(text: "-");
@override
Widget build(BuildContext context) {
+ final isPermanent = model.verificationMethod == kUsePermanentPassword;
return model.isStart
? PaddingCard(
child: Column(
@@ -123,24 +169,23 @@ class _ServerInfoState extends State {
),
TextFormField(
readOnly: true,
- obscureText: !_passwdShow,
style: TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.bold,
color: MyTheme.accent),
- controller: model.serverPasswd,
+ controller: isPermanent ? emptyController : model.serverPasswd,
decoration: InputDecoration(
icon: const Icon(Icons.lock),
labelText: translate("Password"),
labelStyle: TextStyle(
fontWeight: FontWeight.bold, color: MyTheme.accent50),
- suffix: IconButton(
- icon: Icon(Icons.visibility),
- onPressed: () {
- setState(() {
- _passwdShow = !_passwdShow;
- });
- })),
+ suffix: isPermanent
+ ? null
+ : IconButton(
+ icon: const Icon(Icons.refresh),
+ onPressed: () {
+ gFFI.setByName("temporary_password");
+ })),
onSaved: (String? value) {},
),
],
diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart
index 868c599bd..53583479f 100644
--- a/flutter/lib/mobile/pages/settings_page.dart
+++ b/flutter/lib/mobile/pages/settings_page.dart
@@ -1,3 +1,4 @@
+import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
@@ -26,13 +27,100 @@ class SettingsPage extends StatefulWidget implements PageShape {
_SettingsState createState() => _SettingsState();
}
-class _SettingsState extends State {
+class _SettingsState extends State with WidgetsBindingObserver {
static const url = 'https://rustdesk.com/';
+ final _hasIgnoreBattery = androidVersion >= 26;
+ var _ignoreBatteryOpt = false;
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addObserver(this);
+ if (_hasIgnoreBattery) {
+ updateIgnoreBatteryStatus();
+ }
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ super.dispose();
+ }
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if (state == AppLifecycleState.resumed) {
+ updateIgnoreBatteryStatus();
+ }
+ }
+
+ Future updateIgnoreBatteryStatus() async {
+ final res = await PermissionManager.check("ignore_battery_optimizations");
+ if (_ignoreBatteryOpt != res) {
+ setState(() {
+ _ignoreBatteryOpt = res;
+ });
+ return true;
+ } else {
+ return false;
+ }
+ }
@override
Widget build(BuildContext context) {
Provider.of(context);
final username = getUsername();
+ final enableAbr = gFFI.getByName("option", "enable-abr") != 'N';
+ final enhancementsTiles = [
+ SettingsTile.switchTile(
+ title: Text(translate('Adaptive Bitrate') + '(beta)'),
+ initialValue: enableAbr,
+ onToggle: (v) {
+ final msg = Map()
+ ..["name"] = "enable-abr"
+ ..["value"] = "";
+ if (!v) {
+ msg["value"] = "N";
+ }
+ gFFI.setByName("option", json.encode(msg));
+ setState(() {});
+ },
+ )
+ ];
+ if (_hasIgnoreBattery) {
+ enhancementsTiles.insert(
+ 0,
+ SettingsTile.switchTile(
+ initialValue: _ignoreBatteryOpt,
+ title: Text(translate('Keep RustDesk background service')),
+ description:
+ Text('* ${translate('Ignore Battery Optimizations')}'),
+ onToggle: (v) async {
+ if (v) {
+ PermissionManager.request("ignore_battery_optimizations");
+ } else {
+ final res = await DialogManager.show(
+ (setState, close) => CustomAlertDialog(
+ title: Text(translate("Open System Setting")),
+ content: Text(translate(
+ "android_open_battery_optimizations_tip")),
+ actions: [
+ TextButton(
+ onPressed: () => close(),
+ child: Text(translate("Cancel"))),
+ ElevatedButton(
+ onPressed: () => close(true),
+ child:
+ Text(translate("Open System Setting"))),
+ ],
+ ));
+ if (res == true) {
+ PermissionManager.request("application_details_settings");
+ }
+ }
+ }));
+ }
+
return SettingsList(
sections: [
SettingsSection(
@@ -53,17 +141,17 @@ class _SettingsState extends State {
),
],
),
- SettingsSection(
- title: Text(translate("Settings")),
- tiles: [
- SettingsTile.navigation(
+ SettingsSection(title: Text(translate("Settings")), tiles: [
+ SettingsTile.navigation(
title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud),
onPressed: (context) {
showServerSettings();
- },
- ),
- ],
+ })
+ ]),
+ SettingsSection(
+ title: Text(translate("Enhancements")),
+ tiles: enhancementsTiles,
),
SettingsSection(
title: Text(translate("About")),
diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart
index c1e8a31e5..3ab0489a9 100644
--- a/flutter/lib/mobile/widgets/dialog.dart
+++ b/flutter/lib/mobile/widgets/dialog.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -21,9 +22,10 @@ void showError({Duration duration = SEC1}) {
showToast(translate("Error"), duration: SEC1);
}
-void updatePasswordDialog() {
- final p0 = TextEditingController();
- final p1 = TextEditingController();
+void setPermanentPasswordDialog() {
+ final pw = gFFI.getByName("permanent_password");
+ final p0 = TextEditingController(text: pw);
+ final p1 = TextEditingController(text: pw);
var validateLength = false;
var validateSame = false;
DialogManager.show((setState, close) {
@@ -87,7 +89,7 @@ void updatePasswordDialog() {
? () async {
close();
showLoading(translate("Waiting"));
- if (await gFFI.serverModel.updatePassword(p0.text)) {
+ if (await gFFI.serverModel.setPermanentPassword(p0.text)) {
showSuccess();
} else {
showError();
@@ -101,6 +103,41 @@ void updatePasswordDialog() {
});
}
+void setTemporaryPasswordLengthDialog() {
+ List lengths = ['6', '8', '10'];
+ String length = gFFI.getByName('option', 'temporary-password-length');
+ var index = lengths.indexOf(length);
+ if (index < 0) index = 0;
+ length = lengths[index];
+ DialogManager.show((setState, close) {
+ final setLength = (newValue) {
+ final oldValue = length;
+ if (oldValue == newValue) return;
+ setState(() {
+ length = newValue;
+ });
+ Map msg = Map()
+ ..["name"] = "temporary-password-length"
+ ..["value"] = newValue;
+ gFFI.setByName("option", jsonEncode(msg));
+ gFFI.setByName("temporary_password");
+ Future.delayed(Duration(milliseconds: 200), () {
+ close();
+ showSuccess();
+ });
+ };
+ return CustomAlertDialog(
+ title: Text(translate("Set temporary password length")),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ children:
+ lengths.map((e) => getRadio(e, e, length, setLength)).toList()),
+ actions: [],
+ contentPadding: 14,
+ );
+ }, backDismiss: true, clickMaskDismiss: true);
+}
+
void enterPasswordDialog(String id) {
final controller = TextEditingController();
var remember = gFFI.getByName('remember', id) == 'true';
diff --git a/flutter/lib/mobile/widgets/gestures.dart b/flutter/lib/mobile/widgets/gestures.dart
index 960439678..f689bd9ac 100644
--- a/flutter/lib/mobile/widgets/gestures.dart
+++ b/flutter/lib/mobile/widgets/gestures.dart
@@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
-enum CustomTouchGestureState {
+enum GestureState {
none,
oneFingerPan,
twoFingerScale,
@@ -35,64 +35,41 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate;
GestureDragEndCallback? onThreeFingerVerticalDragEnd;
- var _currentState = CustomTouchGestureState.none;
- Timer? _startEventDebounceTimer;
+ var _currentState = GestureState.none;
+ Timer? _debounceTimer;
void _init() {
debugPrint("CustomTouchGestureRecognizer init");
- onStart = (d) {
- _startEventDebounceTimer?.cancel();
- if (d.pointerCount == 1) {
- _currentState = CustomTouchGestureState.oneFingerPan;
- if (onOneFingerPanStart != null) {
- onOneFingerPanStart!(DragStartDetails(
- localPosition: d.localFocalPoint, globalPosition: d.focalPoint));
- }
- debugPrint("start oneFingerPan");
- } else if (d.pointerCount == 2) {
- if (_currentState == CustomTouchGestureState.threeFingerVerticalDrag) {
- // 3 -> 2 debounce
- _startEventDebounceTimer = Timer(Duration(milliseconds: 200), () {
- _currentState = CustomTouchGestureState.twoFingerScale;
- if (onTwoFingerScaleStart != null) {
- onTwoFingerScaleStart!(ScaleStartDetails(
- localFocalPoint: d.localFocalPoint,
- focalPoint: d.focalPoint));
- }
- debugPrint("debounce start twoFingerScale success");
- });
- }
- _currentState = CustomTouchGestureState.twoFingerScale;
- // startWatchTimer();
- if (onTwoFingerScaleStart != null) {
- onTwoFingerScaleStart!(ScaleStartDetails(
- localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint));
- }
- debugPrint("start twoFingerScale");
- } else if (d.pointerCount == 3) {
- _currentState = CustomTouchGestureState.threeFingerVerticalDrag;
+ // onStart = (d) {};
+ onUpdate = (d) {
+ _debounceTimer?.cancel();
+ if (d.pointerCount == 1 && _currentState != GestureState.oneFingerPan) {
+ onOneFingerStartDebounce(d);
+ } else if (d.pointerCount == 2 &&
+ _currentState != GestureState.twoFingerScale) {
+ onTwoFingerStartDebounce(d);
+ } else if (d.pointerCount == 3 &&
+ _currentState != GestureState.threeFingerVerticalDrag) {
+ _currentState = GestureState.threeFingerVerticalDrag;
if (onThreeFingerVerticalDragStart != null) {
onThreeFingerVerticalDragStart!(
DragStartDetails(globalPosition: d.localFocalPoint));
}
debugPrint("start threeFingerScale");
- // _reset();
}
- };
- onUpdate = (d) {
- if (_currentState != CustomTouchGestureState.none) {
+ if (_currentState != GestureState.none) {
switch (_currentState) {
- case CustomTouchGestureState.oneFingerPan:
+ case GestureState.oneFingerPan:
if (onOneFingerPanUpdate != null) {
onOneFingerPanUpdate!(_getDragUpdateDetails(d));
}
break;
- case CustomTouchGestureState.twoFingerScale:
+ case GestureState.twoFingerScale:
if (onTwoFingerScaleUpdate != null) {
onTwoFingerScaleUpdate!(d);
}
break;
- case CustomTouchGestureState.threeFingerVerticalDrag:
+ case GestureState.threeFingerVerticalDrag:
if (onThreeFingerVerticalDragUpdate != null) {
onThreeFingerVerticalDragUpdate!(_getDragUpdateDetails(d));
}
@@ -105,21 +82,22 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
};
onEnd = (d) {
debugPrint("ScaleGestureRecognizer onEnd");
+ _debounceTimer?.cancel();
// end
switch (_currentState) {
- case CustomTouchGestureState.oneFingerPan:
+ case GestureState.oneFingerPan:
debugPrint("TwoFingerState.pan onEnd");
if (onOneFingerPanEnd != null) {
onOneFingerPanEnd!(_getDragEndDetails(d));
}
break;
- case CustomTouchGestureState.twoFingerScale:
+ case GestureState.twoFingerScale:
debugPrint("TwoFingerState.scale onEnd");
if (onTwoFingerScaleEnd != null) {
onTwoFingerScaleEnd!(d);
}
break;
- case CustomTouchGestureState.threeFingerVerticalDrag:
+ case GestureState.threeFingerVerticalDrag:
debugPrint("ThreeFingerState.vertical onEnd");
if (onThreeFingerVerticalDragEnd != null) {
onThreeFingerVerticalDragEnd!(_getDragEndDetails(d));
@@ -128,10 +106,50 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
default:
break;
}
- _currentState = CustomTouchGestureState.none;
+ _debounceTimer = Timer(Duration(milliseconds: 200), () {
+ _currentState = GestureState.none;
+ });
};
}
+ void onOneFingerStartDebounce(ScaleUpdateDetails d) {
+ final start = (ScaleUpdateDetails d) {
+ _currentState = GestureState.oneFingerPan;
+ if (onOneFingerPanStart != null) {
+ onOneFingerPanStart!(DragStartDetails(
+ localPosition: d.localFocalPoint, globalPosition: d.focalPoint));
+ }
+ };
+ if (_currentState != GestureState.none) {
+ _debounceTimer = Timer(Duration(milliseconds: 200), () {
+ start(d);
+ debugPrint("debounce start oneFingerPan");
+ });
+ } else {
+ start(d);
+ debugPrint("start oneFingerPan");
+ }
+ }
+
+ void onTwoFingerStartDebounce(ScaleUpdateDetails d) {
+ final start = (ScaleUpdateDetails d) {
+ _currentState = GestureState.twoFingerScale;
+ if (onTwoFingerScaleStart != null) {
+ onTwoFingerScaleStart!(ScaleStartDetails(
+ localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint));
+ }
+ };
+ if (_currentState == GestureState.threeFingerVerticalDrag) {
+ _debounceTimer = Timer(Duration(milliseconds: 200), () {
+ start(d);
+ debugPrint("debounce start twoFingerScale");
+ });
+ } else {
+ start(d);
+ debugPrint("start twoFingerScale");
+ }
+ }
+
DragUpdateDetails _getDragUpdateDetails(ScaleUpdateDetails d) =>
DragUpdateDetails(
globalPosition: d.focalPoint,
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index 67313623c..743712324 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -396,19 +396,19 @@ class ImageModel with ChangeNotifier {
}
double get maxScale {
- if (_image == null) return 1.0;
+ if (_image == null) return 1.5;
final size = MediaQueryData.fromWindow(ui.window).size;
final xscale = size.width / _image!.width;
final yscale = size.height / _image!.height;
- return max(1.0, max(xscale, yscale));
+ return max(1.5, max(xscale, yscale));
}
double get minScale {
- if (_image == null) return 1.0;
+ if (_image == null) return 1.5;
final size = MediaQueryData.fromWindow(ui.window).size;
final xscale = size.width / _image!.width;
final yscale = size.height / _image!.height;
- return min(xscale, yscale);
+ return min(xscale, yscale) / 1.5;
}
}
diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart
index c161b52db..8ea9e1c93 100644
--- a/flutter/lib/models/server_model.dart
+++ b/flutter/lib/models/server_model.dart
@@ -11,6 +11,10 @@ import 'model.dart';
const loginDialogTag = "LOGIN";
+const kUseTemporaryPassword = "use-temporary-password";
+const kUsePermanentPassword = "use-permanent-password";
+const kUseBothPasswords = "use-both-passwords";
+
class ServerModel with ChangeNotifier {
bool _isStart = false; // Android MainService status
bool _mediaOk = false;
@@ -18,6 +22,7 @@ class ServerModel with ChangeNotifier {
bool _audioOk = false;
bool _fileOk = false;
int _connectStatus = 0; // Rendezvous Server status
+ String _verificationMethod = "";
late String _emptyIdShow;
late final TextEditingController _serverId;
@@ -37,6 +42,8 @@ class ServerModel with ChangeNotifier {
int get connectStatus => _connectStatus;
+ String get verificationMethod => _verificationMethod;
+
TextEditingController get serverId => _serverId;
TextEditingController get serverPasswd => _serverPasswd;
@@ -112,9 +119,29 @@ class ServerModel with ChangeNotifier {
debugPrint("clients not match!");
updateClientState(res);
}
+
+ updatePasswordModel();
});
}
+ updatePasswordModel() {
+ var update = false;
+ final temporaryPassword = gFFI.getByName("temporary_password");
+ final verificationMethod = gFFI.getByName("option", "verification-method");
+ if (_serverPasswd.text != temporaryPassword) {
+ _serverPasswd.text = temporaryPassword;
+ update = true;
+ }
+
+ if (_verificationMethod != verificationMethod) {
+ _verificationMethod = verificationMethod;
+ update = true;
+ }
+ if (update) {
+ notifyListeners();
+ }
+ }
+
toggleAudio() async {
if (!_audioOk && !await PermissionManager.check("audio")) {
final res = await PermissionManager.request("audio");
@@ -216,7 +243,7 @@ class ServerModel with ChangeNotifier {
parent.target?.ffiModel.updateEventListener("");
await parent.target?.invokeMethod("init_service");
parent.target?.setByName("start_service");
- getIDPasswd();
+ _fetchID();
updateClientState();
if (!Platform.isLinux) {
// current linux is not supported
@@ -242,54 +269,33 @@ class ServerModel with ChangeNotifier {
await parent.target?.invokeMethod("init_input");
}
- Future updatePassword(String pw) async {
- final oldPasswd = _serverPasswd.text;
- parent.target?.setByName("update_password", pw);
+ Future setPermanentPassword(String newPW) async {
+ parent.target?.setByName("permanent_password", newPW);
await Future.delayed(Duration(milliseconds: 500));
- await getIDPasswd(force: true);
-
- // check result
- if (pw == "") {
- if (_serverPasswd.text.isNotEmpty && _serverPasswd.text != oldPasswd) {
- return true;
- } else {
- return false;
- }
+ final pw = parent.target?.getByName("permanent_password", newPW);
+ if (newPW == pw) {
+ return true;
} else {
- if (_serverPasswd.text == pw) {
- return true;
- } else {
- return false;
- }
+ return false;
}
}
- getIDPasswd({bool force = false}) async {
- if (!force && _serverId.text != _emptyIdShow && _serverPasswd.text != "") {
- return;
- }
+ _fetchID() async {
+ final old = _serverId.text;
var count = 0;
const maxCount = 10;
while (count < maxCount) {
await Future.delayed(Duration(seconds: 1));
final id = parent.target?.getByName("server_id") ?? "";
- final passwd = parent.target?.getByName("server_password") ?? "";
if (id.isEmpty) {
continue;
} else {
_serverId.text = id;
}
- if (passwd.isEmpty) {
- continue;
- } else {
- _serverPasswd.text = passwd;
- }
-
- debugPrint(
- "fetch id & passwd again at $count:id:${_serverId.text},passwd:${_serverPasswd.text}");
+ debugPrint("fetch id again at $count:id:${_serverId.text}");
count++;
- if (_serverId.text != _emptyIdShow && _serverPasswd.text.isNotEmpty) {
+ if (_serverId.text != old) {
break;
}
}
diff --git a/lang.py b/lang.py
index aff87d411..c7005a055 100644
--- a/lang.py
+++ b/lang.py
@@ -1,17 +1,17 @@
#!/usr/bin/env python3
-# Based on 'cn.rs', generate entries that are not completed in other languages
-
-import os
-import glob
+import os
+import glob
+import sys
+import csv
-def get_lang(lang):
+def get_lang(lang):
out = {}
- for ln in open('./src/lang/%s.rs'%lang):
+ for ln in open('./src/lang/%s.rs'%lang):
ln = ln.strip()
if ln.startswith('("'):
- k,v = line_split(ln)
- out[k] = v
+ k, v = line_split(ln)
+ out[k] = v
return out
def line_split(line):
@@ -19,28 +19,64 @@ def line_split(line):
assert(len(toks) == 2)
k = toks[0][2:]
v = toks[1][:-3]
- return k,v
+ return k, v
-def main():
+def main():
+ if len(sys.argv) == 1:
+ expand()
+ elif sys.argv[1] == '1':
+ to_csv()
+ else:
+ to_rs(sys.argv[1])
+
+
+def expand():
for fn in glob.glob('./src/lang/*'):
lang = os.path.basename(fn)[:-3]
if lang in ['en','cn']: continue
- fw = open("%s.rs.gen"%lang, "wb+")
dict = get_lang(lang)
+ fw = open("%s.rs"%lang, "wt")
for line in open('./src/lang/cn.rs'):
line_strip = line.strip()
if line_strip.startswith('("'):
- k,v = line_split(line_strip)
+ k, v = line_split(line_strip)
if k in dict:
- line = line.replace(v, dict[k])
+ line = line.replace(v, dict[k])
else:
- line = line.replace(v, "")
- fw.write(line.encode())
+ line = line.replace(v, "")
+ fw.write(line)
else:
- fw.write(line.encode())
+ fw.write(line)
fw.close()
- os.remove("./src/lang/%s.rs"%lang)
- os.rename(fw.name, "./src/lang/%s.rs"%lang)
-
-main()
\ No newline at end of file
+
+
+def to_csv():
+ for fn in glob.glob('./src/lang/*.rs'):
+ lang = os.path.basename(fn)[:-3]
+ csvfile = open('./src/lang/%s.csv'%lang, "wt")
+ csvwriter = csv.writer(csvfile)
+ for line in open(fn):
+ line_strip = line.strip()
+ if line_strip.startswith('("'):
+ k, v = line_split(line_strip)
+ csvwriter.writerow([k, v])
+ csvfile.close()
+
+
+def to_rs(lang):
+ csvfile = open('%s.csv'%lang, "rt")
+ fw = open("./src/lang/%s.rs"%lang, "wt")
+ fw.write('''lazy_static::lazy_static! {
+pub static ref T: std::collections::HashMap<&'static str, &'static str> =
+ [
+''')
+ for row in csv.reader(csvfile):
+ fw.write(' ("%s", "%s"),\n'%(row[0].replace('"', '\"'), row[1].replace('"', '\"')))
+ fw.write(''' ].iter().cloned().collect();
+}
+''')
+ fw.close()
+
+
+main()
diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml
index 6842dab56..b0028b564 100644
--- a/libs/enigo/Cargo.toml
+++ b/libs/enigo/Cargo.toml
@@ -22,6 +22,7 @@ appveyor = { repository = "pythoneer/enigo-85xiy" }
serde = { version = "1.0", optional = true }
serde_derive = { version = "1.0", optional = true }
log = "0.4"
+hbb_common = { path = "../hbb_common" }
[features]
with_serde = ["serde", "serde_derive"]
diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs
index 10cde9cbe..164fb1c17 100644
--- a/libs/enigo/src/lib.rs
+++ b/libs/enigo/src/lib.rs
@@ -249,7 +249,7 @@ pub trait MouseControllable {
/// For alphabetical keys, use Key::Layout for a system independent key.
/// If a key is missing, you can use the raw keycode with Key::Raw.
#[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))]
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Key {
/// alt key on Linux and Windows (option key on macOS)
Alt,
diff --git a/libs/enigo/src/linux/mod.rs b/libs/enigo/src/linux/mod.rs
new file mode 100644
index 000000000..42e1dfebf
--- /dev/null
+++ b/libs/enigo/src/linux/mod.rs
@@ -0,0 +1,5 @@
+mod nix_impl;
+mod pynput;
+mod xdo;
+
+pub use self::nix_impl::Enigo;
diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs
new file mode 100644
index 000000000..840290b2b
--- /dev/null
+++ b/libs/enigo/src/linux/nix_impl.rs
@@ -0,0 +1,178 @@
+use super::{pynput::EnigoPynput, xdo::EnigoXdo};
+use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
+
+/// The main struct for handling the event emitting
+// #[derive(Default)]
+pub struct Enigo {
+ xdo: EnigoXdo,
+ pynput: EnigoPynput,
+ is_x11: bool,
+ uinput_keyboard: Option>,
+ uinput_mouse: Option>,
+}
+
+impl Enigo {
+ /// Get delay of xdo implementation.
+ pub fn delay(&self) -> u64 {
+ self.xdo.delay()
+ }
+ /// Set delay of xdo implemetation.
+ pub fn set_delay(&mut self, delay: u64) {
+ self.xdo.set_delay(delay)
+ }
+ /// Reset pynput.
+ pub fn reset(&mut self) {
+ self.pynput.reset();
+ }
+ /// Set uinput keyboard.
+ pub fn set_uinput_keyboard(
+ &mut self,
+ uinput_keyboard: Option>,
+ ) {
+ self.uinput_keyboard = uinput_keyboard
+ }
+ /// Set uinput mouse.
+ pub fn set_uinput_mouse(&mut self, uinput_mouse: Option>) {
+ self.uinput_mouse = uinput_mouse
+ }
+}
+
+impl Default for Enigo {
+ fn default() -> Self {
+ Self {
+ is_x11: "x11" == hbb_common::platform::linux::get_display_server(),
+ uinput_keyboard: None,
+ uinput_mouse: None,
+ xdo: EnigoXdo::default(),
+ pynput: EnigoPynput::default(),
+ }
+ }
+}
+
+impl MouseControllable for Enigo {
+ fn mouse_move_to(&mut self, x: i32, y: i32) {
+ if self.is_x11 {
+ self.xdo.mouse_move_to(x, y);
+ } else {
+ if let Some(mouse) = &mut self.uinput_mouse {
+ mouse.mouse_move_to(x, y)
+ }
+ }
+ }
+ fn mouse_move_relative(&mut self, x: i32, y: i32) {
+ if self.is_x11 {
+ self.xdo.mouse_move_relative(x, y);
+ } else {
+ if let Some(mouse) = &mut self.uinput_mouse {
+ mouse.mouse_move_relative(x, y)
+ }
+ }
+ }
+ fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType {
+ if self.is_x11 {
+ self.xdo.mouse_down(button)
+ } else {
+ if let Some(mouse) = &mut self.uinput_mouse {
+ mouse.mouse_down(button)
+ } else {
+ Ok(())
+ }
+ }
+ }
+ fn mouse_up(&mut self, button: MouseButton) {
+ if self.is_x11 {
+ self.xdo.mouse_up(button)
+ } else {
+ if let Some(mouse) = &mut self.uinput_mouse {
+ mouse.mouse_up(button)
+ }
+ }
+ }
+ fn mouse_click(&mut self, button: MouseButton) {
+ if self.is_x11 {
+ self.xdo.mouse_click(button)
+ } else {
+ if let Some(mouse) = &mut self.uinput_mouse {
+ mouse.mouse_click(button)
+ }
+ }
+ }
+ fn mouse_scroll_x(&mut self, length: i32) {
+ if self.is_x11 {
+ self.xdo.mouse_scroll_x(length)
+ } else {
+ if let Some(mouse) = &mut self.uinput_mouse {
+ mouse.mouse_scroll_x(length)
+ }
+ }
+ }
+ fn mouse_scroll_y(&mut self, length: i32) {
+ if self.is_x11 {
+ self.xdo.mouse_scroll_y(length)
+ } else {
+ if let Some(mouse) = &mut self.uinput_mouse {
+ mouse.mouse_scroll_y(length)
+ }
+ }
+ }
+}
+
+impl KeyboardControllable for Enigo {
+ fn get_key_state(&mut self, key: Key) -> bool {
+ if self.is_x11 {
+ self.xdo.get_key_state(key)
+ } else {
+ if let Some(keyboard) = &mut self.uinput_keyboard {
+ keyboard.get_key_state(key)
+ } else {
+ false
+ }
+ }
+ }
+
+ fn key_sequence(&mut self, sequence: &str) {
+ if self.is_x11 {
+ self.xdo.key_sequence(sequence)
+ } else {
+ if let Some(keyboard) = &mut self.uinput_keyboard {
+ keyboard.key_sequence(sequence)
+ }
+ }
+ }
+
+ fn key_down(&mut self, key: Key) -> crate::ResultType {
+ if self.is_x11 {
+ if self.pynput.send_pynput(&key, true) {
+ return Ok(());
+ }
+ self.xdo.key_down(key)
+ } else {
+ if let Some(keyboard) = &mut self.uinput_keyboard {
+ keyboard.key_down(key)
+ } else {
+ Ok(())
+ }
+ }
+ }
+ fn key_up(&mut self, key: Key) {
+ if self.is_x11 {
+ if self.pynput.send_pynput(&key, false) {
+ return;
+ }
+ self.xdo.key_up(key)
+ } else {
+ if let Some(keyboard) = &mut self.uinput_keyboard {
+ keyboard.key_up(key)
+ }
+ }
+ }
+ fn key_click(&mut self, key: Key) {
+ if self.is_x11 {
+ self.xdo.key_click(key)
+ } else {
+ if let Some(keyboard) = &mut self.uinput_keyboard {
+ keyboard.key_click(key)
+ }
+ }
+ }
+}
diff --git a/libs/enigo/src/linux/pynput.rs b/libs/enigo/src/linux/pynput.rs
new file mode 100644
index 000000000..748b30105
--- /dev/null
+++ b/libs/enigo/src/linux/pynput.rs
@@ -0,0 +1,280 @@
+use crate::Key;
+use std::{io::prelude::*, sync::mpsc};
+
+enum PyMsg {
+ Char(char),
+ Str(&'static str),
+}
+
+/// The main struct for handling the event emitting
+pub(super) struct EnigoPynput {
+ tx: mpsc::Sender<(PyMsg, bool)>,
+}
+
+impl Default for EnigoPynput {
+ fn default() -> Self {
+ let (tx, rx) = mpsc::channel();
+ start_pynput_service(rx);
+ Self { tx }
+ }
+}
+impl EnigoPynput {
+ pub(super) fn reset(&mut self) {
+ self.tx.send((PyMsg::Char('\0'), true)).ok();
+ }
+
+ #[inline]
+ pub(super) fn send_pynput(&mut self, key: &Key, is_press: bool) -> bool {
+ if unsafe { PYNPUT_EXIT || !PYNPUT_REDAY } {
+ return false;
+ }
+ if let Key::Layout(c) = key {
+ return self.tx.send((PyMsg::Char(*c), is_press)).is_ok();
+ }
+ if let Key::Raw(_) = key {
+ return false;
+ }
+ #[allow(deprecated)]
+ let s = match key {
+ Key::Alt => "Alt_L",
+ Key::Backspace => "BackSpace",
+ Key::CapsLock => "Caps_Lock",
+ Key::Control => "Control_L",
+ Key::Delete => "Delete",
+ Key::DownArrow => "Down",
+ Key::End => "End",
+ Key::Escape => "Escape",
+ Key::F1 => "F1",
+ Key::F10 => "F10",
+ Key::F11 => "F11",
+ Key::F12 => "F12",
+ Key::F2 => "F2",
+ Key::F3 => "F3",
+ Key::F4 => "F4",
+ Key::F5 => "F5",
+ Key::F6 => "F6",
+ Key::F7 => "F7",
+ Key::F8 => "F8",
+ Key::F9 => "F9",
+ Key::Home => "Home",
+ Key::LeftArrow => "Left",
+ Key::Option => "Option",
+ Key::PageDown => "Page_Down",
+ Key::PageUp => "Page_Up",
+ Key::Return => "Return",
+ Key::RightArrow => "Right",
+ Key::Shift => "Shift_L",
+ Key::Space => "space",
+ Key::Tab => "Tab",
+ Key::UpArrow => "Up",
+ Key::Numpad0 => "0",
+ Key::Numpad1 => "1",
+ Key::Numpad2 => "2",
+ Key::Numpad3 => "3",
+ Key::Numpad4 => "4",
+ Key::Numpad5 => "5",
+ Key::Numpad6 => "6",
+ Key::Numpad7 => "7",
+ Key::Numpad8 => "8",
+ Key::Numpad9 => "9",
+ Key::Decimal => "KP_Decimal",
+ Key::Cancel => "Cancel",
+ Key::Clear => "Clear",
+ Key::Pause => "Pause",
+ Key::Kana => "Kana",
+ Key::Hangul => "Hangul",
+ Key::Hanja => "Hanja",
+ Key::Kanji => "Kanji",
+ Key::Select => "Select",
+ Key::Print => "Print",
+ Key::Execute => "Execute",
+ Key::Snapshot => "3270_PrintScreen",
+ Key::Insert => "Insert",
+ Key::Help => "Help",
+ Key::Separator => "KP_Separator",
+ Key::Scroll => "Scroll_Lock",
+ Key::NumLock => "Num_Lock",
+ Key::RWin => "Super_R",
+ Key::Apps => "Menu",
+ Key::Multiply => "KP_Multiply",
+ Key::Add => "KP_Add",
+ Key::Subtract => "KP_Subtract",
+ Key::Divide => "KP_Divide",
+ Key::Equals => "KP_Equal",
+ Key::NumpadEnter => "KP_Enter",
+ Key::RightShift => "Shift_R",
+ Key::RightControl => "Control_R",
+ Key::RightAlt => "Mode_switch",
+ Key::Command | Key::Super | Key::Windows | Key::Meta => "Super_L",
+ _ => {
+ return true;
+ }
+ };
+ log::info!("send pynput: {:?}", &s);
+ return self.tx.send((PyMsg::Str(s), is_press)).is_ok();
+ }
+}
+
+// impl MouseControllable for EnigoPynput {
+// fn mouse_move_to(&mut self, _x: i32, _y: i32) {
+// unimplemented!()
+// }
+// fn mouse_move_relative(&mut self, _x: i32, _y: i32) {
+// unimplemented!()
+// }
+// fn mouse_down(&mut self, _button: MouseButton) -> crate::ResultType {
+// unimplemented!()
+// }
+// fn mouse_up(&mut self, _button: MouseButton) {
+// unimplemented!()
+// }
+// fn mouse_click(&mut self, _button: MouseButton) {
+// unimplemented!()
+// }
+// fn mouse_scroll_x(&mut self, _length: i32) {
+// unimplemented!()
+// }
+// fn mouse_scroll_y(&mut self, _length: i32) {
+// unimplemented!()
+// }
+// }
+
+// impl KeyboardControllable for EnigoPynput {
+// fn get_key_state(&mut self, _key: Key) -> bool {
+// unimplemented!()
+// }
+
+// fn key_sequence(&mut self, _sequence: &str) {
+// unimplemented!()
+// }
+// fn key_down(&mut self, key: Key) -> crate::ResultType {
+// let _ = self.send_pynput(&key, true);
+// Ok(())
+// }
+// fn key_up(&mut self, key: Key) {
+// let _ = self.send_pynput(&key, false);
+// }
+// fn key_click(&mut self, _key: Key) {
+// unimplemented!()
+// }
+// }
+
+static mut PYNPUT_EXIT: bool = false;
+static mut PYNPUT_REDAY: bool = false;
+static IPC_FILE: &'static str = "/tmp/RustDesk/pynput_service";
+
+fn start_pynput_service(rx: mpsc::Receiver<(PyMsg, bool)>) {
+ let mut py = "./pynput_service.py".to_owned();
+ if !std::path::Path::new(&py).exists() {
+ py = "/usr/share/rustdesk/files/pynput_service.py".to_owned();
+ if !std::path::Path::new(&py).exists() {
+ py = "/usr/lib/rustdesk/pynput_service.py".to_owned();
+ if !std::path::Path::new(&py).exists() {
+ log::error!("{} not exits", py);
+ }
+ }
+ }
+ log::info!("pynput service: {}", py);
+ std::thread::spawn(move || {
+ let username = std::env::var("PYNPUT_USERNAME").unwrap_or("".to_owned());
+ let userid = std::env::var("PYNPUT_USERID").unwrap_or("".to_owned());
+ let status = if username.is_empty() {
+ std::process::Command::new("python3")
+ .arg(&py)
+ .arg(IPC_FILE)
+ .status()
+ .map(|x| x.success())
+ } else {
+ let mut status = Ok(true);
+ for i in 0..100 {
+ if i % 10 == 0 {
+ log::info!("#{} try to start pynput server", i);
+ }
+ status = std::process::Command::new("sudo")
+ .args(vec![
+ "-E",
+ &format!("XDG_RUNTIME_DIR=/run/user/{}", userid) as &str,
+ "-u",
+ &username,
+ "python3",
+ &py,
+ IPC_FILE,
+ ])
+ .status()
+ .map(|x| x.success());
+ match status {
+ Ok(true) => break,
+ _ => {}
+ }
+ std::thread::sleep(std::time::Duration::from_millis(100));
+ }
+ status
+ };
+ log::info!(
+ "pynput server exit with username/id {}/{}: {:?}",
+ username,
+ userid,
+ status
+ );
+ unsafe {
+ PYNPUT_EXIT = true;
+ }
+ });
+ std::thread::spawn(move || {
+ for i in 0..300 {
+ std::thread::sleep(std::time::Duration::from_millis(100));
+ let mut conn = match std::os::unix::net::UnixStream::connect(IPC_FILE) {
+ Ok(conn) => conn,
+ Err(err) => {
+ if i % 15 == 0 {
+ log::warn!("Failed to connect to {}: {}", IPC_FILE, err);
+ }
+ continue;
+ }
+ };
+ if let Err(err) = conn.set_nonblocking(true) {
+ log::error!("Failed to set ipc nonblocking: {}", err);
+ return;
+ }
+ log::info!("Conntected to pynput server");
+ let d = std::time::Duration::from_millis(30);
+ unsafe {
+ PYNPUT_REDAY = true;
+ }
+ let mut buf = [0u8; 1024];
+ loop {
+ if unsafe { PYNPUT_EXIT } {
+ break;
+ }
+ match rx.recv_timeout(d) {
+ Ok((msg, is_press)) => {
+ let msg = match msg {
+ PyMsg::Char(chr) => {
+ format!("{}{}", if is_press { 'p' } else { 'r' }, chr)
+ }
+ PyMsg::Str(s) => format!("{}{}", if is_press { 'p' } else { 'r' }, s),
+ };
+ let n = msg.len();
+ buf[0] = n as _;
+ buf[1..(n + 1)].copy_from_slice(msg.as_bytes());
+ if let Err(err) = conn.write_all(&buf[..n + 1]) {
+ log::error!("Failed to write to ipc: {}", err);
+ break;
+ }
+ }
+ Err(err) => match err {
+ mpsc::RecvTimeoutError::Disconnected => {
+ log::error!("pynput sender disconnecte");
+ break;
+ }
+ _ => {}
+ },
+ }
+ }
+ unsafe {
+ PYNPUT_REDAY = false;
+ }
+ break;
+ }
+ });
+}
diff --git a/libs/enigo/src/linux.rs b/libs/enigo/src/linux/xdo.rs
similarity index 54%
rename from libs/enigo/src/linux.rs
rename to libs/enigo/src/linux/xdo.rs
index adfe9507c..0e3b79ab3 100644
--- a/libs/enigo/src/linux.rs
+++ b/libs/enigo/src/linux/xdo.rs
@@ -3,7 +3,7 @@ use libc;
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
use self::libc::{c_char, c_int, c_void, useconds_t};
-use std::{borrow::Cow, ffi::CString, io::prelude::*, ptr, sync::mpsc};
+use std::{borrow::Cow, ffi::CString, ptr};
const CURRENT_WINDOW: c_int = 0;
const DEFAULT_DELAY: u64 = 12000;
@@ -60,34 +60,25 @@ fn mousebutton(button: MouseButton) -> c_int {
}
}
-enum PyMsg {
- Char(char),
- Str(&'static str),
-}
-
/// The main struct for handling the event emitting
-pub struct Enigo {
+pub(super) struct EnigoXdo {
xdo: Xdo,
delay: u64,
- tx: mpsc::Sender<(PyMsg, bool)>,
}
// This is safe, we have a unique pointer.
// TODO: use Unique once stable.
-unsafe impl Send for Enigo {}
+unsafe impl Send for EnigoXdo {}
-impl Default for Enigo {
- /// Create a new Enigo instance
+impl Default for EnigoXdo {
+ /// Create a new EnigoXdo instance
fn default() -> Self {
- let (tx, rx) = mpsc::channel();
- start_pynput_service(rx);
Self {
xdo: unsafe { xdo_new(ptr::null()) },
delay: DEFAULT_DELAY,
- tx,
}
}
}
-impl Enigo {
+impl EnigoXdo {
/// Get the delay per keypress.
/// Default value is 12000.
/// This is Linux-specific.
@@ -99,101 +90,8 @@ impl Enigo {
pub fn set_delay(&mut self, delay: u64) {
self.delay = delay;
}
- ///
- pub fn reset(&mut self) {
- self.tx.send((PyMsg::Char('\0'), true)).ok();
- }
- #[inline]
- fn send_pynput(&mut self, key: &Key, is_press: bool) -> bool {
- if unsafe { PYNPUT_EXIT || !PYNPUT_REDAY } {
- return false;
- }
- if let Key::Layout(c) = key {
- return self.tx.send((PyMsg::Char(*c), is_press)).is_ok();
- }
- if let Key::Raw(_) = key {
- return false;
- }
- #[allow(deprecated)]
- let s = match key {
- Key::Alt => "Alt_L",
- Key::Backspace => "BackSpace",
- Key::CapsLock => "Caps_Lock",
- Key::Control => "Control_L",
- Key::Delete => "Delete",
- Key::DownArrow => "Down",
- Key::End => "End",
- Key::Escape => "Escape",
- Key::F1 => "F1",
- Key::F10 => "F10",
- Key::F11 => "F11",
- Key::F12 => "F12",
- Key::F2 => "F2",
- Key::F3 => "F3",
- Key::F4 => "F4",
- Key::F5 => "F5",
- Key::F6 => "F6",
- Key::F7 => "F7",
- Key::F8 => "F8",
- Key::F9 => "F9",
- Key::Home => "Home",
- Key::LeftArrow => "Left",
- Key::Option => "Option",
- Key::PageDown => "Page_Down",
- Key::PageUp => "Page_Up",
- Key::Return => "Return",
- Key::RightArrow => "Right",
- Key::Shift => "Shift_L",
- Key::Space => "space",
- Key::Tab => "Tab",
- Key::UpArrow => "Up",
- Key::Numpad0 => "0",
- Key::Numpad1 => "1",
- Key::Numpad2 => "2",
- Key::Numpad3 => "3",
- Key::Numpad4 => "4",
- Key::Numpad5 => "5",
- Key::Numpad6 => "6",
- Key::Numpad7 => "7",
- Key::Numpad8 => "8",
- Key::Numpad9 => "9",
- Key::Decimal => "KP_Decimal",
- Key::Cancel => "Cancel",
- Key::Clear => "Clear",
- Key::Pause => "Pause",
- Key::Kana => "Kana",
- Key::Hangul => "Hangul",
- Key::Hanja => "Hanja",
- Key::Kanji => "Kanji",
- Key::Select => "Select",
- Key::Print => "Print",
- Key::Execute => "Execute",
- Key::Snapshot => "3270_PrintScreen",
- Key::Insert => "Insert",
- Key::Help => "Help",
- Key::Separator => "KP_Separator",
- Key::Scroll => "Scroll_Lock",
- Key::NumLock => "Num_Lock",
- Key::RWin => "Super_R",
- Key::Apps => "Menu",
- Key::Multiply => "KP_Multiply",
- Key::Add => "KP_Add",
- Key::Subtract => "KP_Subtract",
- Key::Divide => "KP_Divide",
- Key::Equals => "KP_Equal",
- Key::NumpadEnter => "KP_Enter",
- Key::RightShift => "Shift_R",
- Key::RightControl => "Control_R",
- Key::RightAlt => "Mode_switch",
- Key::Command | Key::Super | Key::Windows | Key::Meta => "Super_L",
- _ => {
- return true;
- }
- };
- return self.tx.send((PyMsg::Str(s), is_press)).is_ok();
- }
}
-impl Drop for Enigo {
+impl Drop for EnigoXdo {
fn drop(&mut self) {
if self.xdo.is_null() {
return;
@@ -203,7 +101,7 @@ impl Drop for Enigo {
}
}
}
-impl MouseControllable for Enigo {
+impl MouseControllable for EnigoXdo {
fn mouse_move_to(&mut self, x: i32, y: i32) {
if self.xdo.is_null() {
return;
@@ -378,7 +276,7 @@ fn keysequence<'a>(key: Key) -> Cow<'a, str> {
_ => "",
})
}
-impl KeyboardControllable for Enigo {
+impl KeyboardControllable for EnigoXdo {
fn get_key_state(&mut self, key: Key) -> bool {
if self.xdo.is_null() {
return false;
@@ -431,9 +329,6 @@ impl KeyboardControllable for Enigo {
if self.xdo.is_null() {
return Ok(());
}
- if self.send_pynput(&key, true) {
- return Ok(());
- }
let string = CString::new(&*keysequence(key))?;
unsafe {
xdo_send_keysequence_window_down(
@@ -449,9 +344,6 @@ impl KeyboardControllable for Enigo {
if self.xdo.is_null() {
return;
}
- if self.send_pynput(&key, false) {
- return;
- }
if let Ok(string) = CString::new(&*keysequence(key)) {
unsafe {
xdo_send_keysequence_window_up(
@@ -479,127 +371,3 @@ impl KeyboardControllable for Enigo {
}
}
}
-
-static mut PYNPUT_EXIT: bool = false;
-static mut PYNPUT_REDAY: bool = false;
-static IPC_FILE: &'static str = "/tmp/RustDesk/pynput_service";
-
-fn start_pynput_service(rx: mpsc::Receiver<(PyMsg, bool)>) {
- let mut py = "./pynput_service.py".to_owned();
- if !std::path::Path::new(&py).exists() {
- py = "/usr/share/rustdesk/files/pynput_service.py".to_owned();
- if !std::path::Path::new(&py).exists() {
- py = "/usr/lib/rustdesk/pynput_service.py".to_owned();
- if !std::path::Path::new(&py).exists() {
- // enigo libs, not rustdesk root project, so skip using appimage features
- py = std::env::var("APPDIR").unwrap_or("".to_string()) + "/usr/lib/rustdesk/pynput_service.py";
- if !std::path::Path::new(&py).exists() {
- log::error!("{} not exists", py);
- }
- }
- }
- }
- log::info!("pynput service: {}", py);
- std::thread::spawn(move || {
- let username = std::env::var("PYNPUT_USERNAME").unwrap_or("".to_owned());
- let userid = std::env::var("PYNPUT_USERID").unwrap_or("".to_owned());
- let status = if username.is_empty() {
- std::process::Command::new("python3")
- .arg(&py)
- .arg(IPC_FILE)
- .status()
- .map(|x| x.success())
- } else {
- let mut status = Ok(true);
- for i in 0..100 {
- if i % 10 == 0 {
- log::info!("#{} try to start pynput server", i);
- }
- status = std::process::Command::new("sudo")
- .args(vec![
- "-E",
- &format!("XDG_RUNTIME_DIR=/run/user/{}", userid) as &str,
- "-u",
- &username,
- "python3",
- &py,
- IPC_FILE,
- ])
- .status()
- .map(|x| x.success());
- match status {
- Ok(true) => break,
- _ => {}
- }
- std::thread::sleep(std::time::Duration::from_millis(100));
- }
- status
- };
- log::info!(
- "pynput server exit with username/id {}/{}: {:?}",
- username,
- userid,
- status
- );
- unsafe {
- PYNPUT_EXIT = true;
- }
- });
- std::thread::spawn(move || {
- for i in 0..300 {
- std::thread::sleep(std::time::Duration::from_millis(100));
- let mut conn = match std::os::unix::net::UnixStream::connect(IPC_FILE) {
- Ok(conn) => conn,
- Err(err) => {
- if i % 15 == 0 {
- log::warn!("Failed to connect to {}: {}", IPC_FILE, err);
- }
- continue;
- }
- };
- if let Err(err) = conn.set_nonblocking(true) {
- log::error!("Failed to set ipc nonblocking: {}", err);
- return;
- }
- log::info!("Conntected to pynput server");
- let d = std::time::Duration::from_millis(30);
- unsafe {
- PYNPUT_REDAY = true;
- }
- let mut buf = [0u8; 1024];
- loop {
- if unsafe { PYNPUT_EXIT } {
- break;
- }
- match rx.recv_timeout(d) {
- Ok((msg, is_press)) => {
- let msg = match msg {
- PyMsg::Char(chr) => {
- format!("{}{}", if is_press { 'p' } else { 'r' }, chr)
- }
- PyMsg::Str(s) => format!("{}{}", if is_press { 'p' } else { 'r' }, s),
- };
- let n = msg.len();
- buf[0] = n as _;
- buf[1..(n + 1)].copy_from_slice(msg.as_bytes());
- if let Err(err) = conn.write_all(&buf[..n + 1]) {
- log::error!("Failed to write to ipc: {}", err);
- break;
- }
- }
- Err(err) => match err {
- mpsc::RecvTimeoutError::Disconnected => {
- log::error!("pynput sender disconnecte");
- break;
- }
- _ => {}
- },
- }
- }
- unsafe {
- PYNPUT_REDAY = false;
- }
- break;
- }
- });
-}
diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml
index bc31223cc..6773c0f53 100644
--- a/libs/hbb_common/Cargo.toml
+++ b/libs/hbb_common/Cargo.toml
@@ -7,11 +7,11 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-protobuf = "3.0.0-alpha.2"
-tokio = { version = "1.15", features = ["full"] }
-tokio-util = { version = "0.6", features = ["full"] }
+protobuf = { version = "3.1", features = ["with-bytes"] }
+tokio = { version = "1.20", features = ["full"] }
+tokio-util = { version = "0.7", features = ["full"] }
futures = "0.3"
-bytes = "1.1"
+bytes = { version = "1.2", features = ["serde"] }
log = "0.4"
env_logger = "0.9"
socket2 = { version = "0.3", features = ["reuseport"] }
@@ -23,6 +23,7 @@ directories-next = "2.0"
rand = "0.8"
serde_derive = "1.0"
serde = "1.0"
+serde_with = "1.14.0"
lazy_static = "1.4"
confy = { git = "https://github.com/open-trade/confy" }
dirs-next = "2.0"
@@ -33,12 +34,13 @@ tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1"
+machine-uid = "0.2"
[features]
quic = []
[build-dependencies]
-protobuf-codegen-pure = "3.0.0-alpha.2"
+protobuf-codegen = { version = "3.1" }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }
diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs
index 99dacb7ec..225ec34c7 100644
--- a/libs/hbb_common/build.rs
+++ b/libs/hbb_common/build.rs
@@ -1,9 +1,14 @@
fn main() {
std::fs::create_dir_all("src/protos").unwrap();
- protobuf_codegen_pure::Codegen::new()
+ protobuf_codegen::Codegen::new()
+ .pure()
.out_dir("src/protos")
.inputs(&["protos/rendezvous.proto", "protos/message.proto"])
.include("protos")
+ .customize(
+ protobuf_codegen::Customize::default()
+ .tokio_bytes(true)
+ )
.run()
.expect("Codegen failed.");
}
diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto
index 048301d7e..5069fa2b0 100644
--- a/libs/hbb_common/protos/message.proto
+++ b/libs/hbb_common/protos/message.proto
@@ -1,13 +1,13 @@
syntax = "proto3";
package hbb;
-message VP9 {
+message EncodedVideoFrame {
bytes data = 1;
bool key = 2;
int64 pts = 3;
}
-message VP9s { repeated VP9 frames = 1; }
+message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; }
message RGB { bool compress = 1; }
@@ -19,9 +19,11 @@ message YUV {
message VideoFrame {
oneof union {
- VP9s vp9s = 6;
+ EncodedVideoFrames vp9s = 6;
RGB rgb = 7;
YUV yuv = 8;
+ EncodedVideoFrames h264s = 10;
+ EncodedVideoFrames h265s = 11;
}
int64 timestamp = 9;
}
@@ -61,6 +63,7 @@ message LoginRequest {
PortForward port_forward = 8;
}
bool video_ack_required = 9;
+ uint64 session_id = 10;
}
message ChatMessage { string text = 1; }
@@ -69,6 +72,11 @@ message Features {
bool privacy_mode = 1;
}
+message SupportedEncoding {
+ bool h264 = 1;
+ bool h265 = 2;
+}
+
message PeerInfo {
string username = 1;
string hostname = 2;
@@ -79,6 +87,7 @@ message PeerInfo {
string version = 7;
int32 conn_id = 8;
Features features = 9;
+ SupportedEncoding encoding = 10;
}
message LoginResponse {
@@ -417,6 +426,7 @@ message PermissionInfo {
Clipboard = 2;
Audio = 3;
File = 4;
+ Restart = 5;
}
Permission permission = 1;
@@ -430,6 +440,20 @@ enum ImageQuality {
Best = 4;
}
+message VideoCodecState {
+ enum PerferCodec {
+ Auto = 0;
+ VPX = 1;
+ H264 = 2;
+ H265 = 3;
+ }
+
+ int32 score_vpx = 1;
+ int32 score_h264 = 2;
+ int32 score_h265 = 3;
+ PerferCodec perfer = 4;
+}
+
message OptionMessage {
enum BoolOption {
NotSet = 0;
@@ -445,11 +469,14 @@ message OptionMessage {
BoolOption disable_audio = 7;
BoolOption disable_clipboard = 8;
BoolOption enable_file_transfer = 9;
+ VideoCodecState video_codec_state = 10;
}
message TestDelay {
int64 time = 1;
bool from_client = 2;
+ uint32 last_delay = 3;
+ uint32 target_bitrate = 4;
}
message PublicKey {
@@ -472,33 +499,33 @@ message AudioFrame {
message BackNotification {
// no need to consider block input by someone else
enum BlockInputState {
- StateUnknown = 1;
- OnSucceeded = 2;
- OnFailed = 3;
- OffSucceeded = 4;
- OffFailed = 5;
+ BlkStateUnknown = 0;
+ BlkOnSucceeded = 2;
+ BlkOnFailed = 3;
+ BlkOffSucceeded = 4;
+ BlkOffFailed = 5;
}
enum PrivacyModeState {
- StateUnknown = 1;
+ PrvStateUnknown = 0;
// Privacy mode on by someone else
- OnByOther = 2;
+ PrvOnByOther = 2;
// Privacy mode is not supported on the remote side
- NotSupported = 3;
+ PrvNotSupported = 3;
// Privacy mode on by self
- OnSucceeded = 4;
+ PrvOnSucceeded = 4;
// Privacy mode on by self, but denied
- OnFailedDenied = 5;
+ PrvOnFailedDenied = 5;
// Some plugins are not found
- OnFailedPlugin = 6;
+ PrvOnFailedPlugin = 6;
// Privacy mode on by self, but failed
- OnFailed = 7;
+ PrvOnFailed = 7;
// Privacy mode off by self
- OffSucceeded = 8;
+ PrvOffSucceeded = 8;
// Ctrl + P
- OffByPeer = 9;
+ PrvOffByPeer = 9;
// Privacy mode off by self, but failed
- OffFailed = 10;
- OffUnknown = 11;
+ PrvOffFailed = 10;
+ PrvOffUnknown = 11;
}
oneof union {
@@ -518,6 +545,7 @@ message Misc {
bool refresh_video = 10;
bool video_received = 12;
BackNotification back_notification = 13;
+ bool restart_remote_device = 14;
}
}
diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs
index 91faf833b..33c46e7ae 100644
--- a/libs/hbb_common/src/config.rs
+++ b/libs/hbb_common/src/config.rs
@@ -1,8 +1,3 @@
-use crate::log;
-use directories_next::ProjectDirs;
-use rand::Rng;
-use serde_derive::{Deserialize, Serialize};
-use sodiumoxide::crypto::sign;
use std::{
collections::HashMap,
fs,
@@ -12,11 +7,26 @@ use std::{
time::SystemTime,
};
+use anyhow::Result;
+use directories_next::ProjectDirs;
+use rand::Rng;
+use serde_derive::{Deserialize, Serialize};
+use sodiumoxide::crypto::sign;
+
+use crate::{
+ log,
+ password_security::{
+ decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original,
+ encrypt_vec_or_original,
+ },
+};
+
pub const RENDEZVOUS_TIMEOUT: u64 = 12_000;
pub const CONNECT_TIMEOUT: u64 = 18_000;
pub const REG_INTERVAL: i64 = 12_000;
pub const COMPRESS_LEVEL: i32 = 3;
const SERIAL: i32 = 3;
+const PASSWORD_ENC_VERSION: &'static str = "00";
// 128x128
#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding
pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII=
@@ -38,7 +48,9 @@ lazy_static::lazy_static! {
pub static ref ONLINE: Arc>> = Default::default();
pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default();
pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned()));
+ static ref KEY_PAIR: Arc, Vec)>>> = Default::default();
}
+
// #[cfg(any(target_os = "android", target_os = "ios"))]
lazy_static::lazy_static! {
pub static ref APP_DIR: Arc> = Default::default();
@@ -54,6 +66,7 @@ pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[
"rs-sg.rustdesk.com",
"rs-cn.rustdesk.com",
];
+pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=";
pub const RENDEZVOUS_PORT: i32 = 21116;
pub const RELAY_PORT: i32 = 21117;
@@ -72,7 +85,7 @@ pub struct Config {
#[serde(default)]
salt: String,
#[serde(default)]
- pub key_pair: (Vec, Vec), // sk, pk
+ key_pair: (Vec, Vec), // sk, pk
#[serde(default)]
key_confirmed: bool,
#[serde(default)]
@@ -107,7 +120,7 @@ pub struct Config2 {
pub options: HashMap,
}
-#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
pub struct PeerConfig {
#[serde(default)]
pub password: Vec,
@@ -139,6 +152,8 @@ pub struct PeerConfig {
pub disable_clipboard: bool,
#[serde(default)]
pub enable_file_transfer: bool,
+ #[serde(default)]
+ pub show_quality_monitor: bool,
// the other scalar value must before this
#[serde(default)]
@@ -159,7 +174,7 @@ pub struct PeerInfoSerde {
pub platform: String,
}
-#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
pub struct TransferSerde {
#[serde(default)]
pub write_jobs: Vec,
@@ -198,7 +213,17 @@ fn patch(path: PathBuf) -> PathBuf {
impl Config2 {
fn load() -> Config2 {
- Config::load_::("2")
+ let mut config = Config::load_::("2");
+ if let Some(mut socks) = config.socks {
+ let (password, _, store) =
+ decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION);
+ socks.password = password;
+ config.socks = Some(socks);
+ if store {
+ config.store();
+ }
+ }
+ config
}
pub fn file() -> PathBuf {
@@ -206,7 +231,12 @@ impl Config2 {
}
fn store(&self) {
- Config::store_(self, "2");
+ let mut config = self.clone();
+ if let Some(mut socks) = config.socks {
+ socks.password = encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION);
+ config.socks = Some(socks);
+ }
+ Config::store_(&config, "2");
}
pub fn get() -> Config2 {
@@ -237,6 +267,11 @@ pub fn load_path(path: PathBuf, cfg: T) -> crate::ResultType<()> {
+ Ok(confy::store_path(path, cfg)?)
+}
+
impl Config {
fn load_(
suffix: &str,
@@ -252,17 +287,25 @@ impl Config {
fn store_(config: &T, suffix: &str) {
let file = Self::file_(suffix);
- if let Err(err) = confy::store_path(file, config) {
+ if let Err(err) = store_path(file, config) {
log::error!("Failed to store config: {}", err);
}
}
fn load() -> Config {
- Config::load_::("")
+ let mut config = Config::load_::("");
+ let (password, _, store) = decrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION);
+ config.password = password;
+ if store {
+ config.store();
+ }
+ config
}
fn store(&self) {
- Config::store_(self, "");
+ let mut config = self.clone();
+ config.password = encrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION);
+ Config::store_(&config, "");
}
pub fn file() -> PathBuf {
@@ -274,6 +317,10 @@ impl Config {
Config::with_extension(Self::path(name))
}
+ pub fn is_empty(&self) -> bool {
+ self.id.is_empty() || self.key_pair.0.is_empty()
+ }
+
pub fn get_home() -> PathBuf {
#[cfg(any(target_os = "android", target_os = "ios"))]
return Self::path(APP_HOME_DIR.read().unwrap().as_str());
@@ -495,9 +542,9 @@ impl Config {
}
}
- pub fn get_auto_password() -> String {
+ pub fn get_auto_password(length: usize) -> String {
let mut rng = rand::thread_rng();
- (0..6)
+ (0..length)
.map(|_| CHARS[rng.gen::() % CHARS.len()])
.collect()
}
@@ -535,24 +582,26 @@ impl Config {
config.store();
}
- pub fn set_key_pair(pair: (Vec, Vec)) {
- let mut config = CONFIG.write().unwrap();
- if config.key_pair == pair {
- return;
- }
- config.key_pair = pair;
- config.store();
- }
-
pub fn get_key_pair() -> (Vec, Vec) {
// lock here to make sure no gen_keypair more than once
- let mut config = CONFIG.write().unwrap();
+ // no use of CONFIG directly here to ensure no recursive calling in Config::load because of password dec which calling this function
+ let mut lock = KEY_PAIR.lock().unwrap();
+ if let Some(p) = lock.as_ref() {
+ return p.clone();
+ }
+ let mut config = Config::load_::("");
if config.key_pair.0.is_empty() {
let (pk, sk) = sign::gen_keypair();
- config.key_pair = (sk.0.to_vec(), pk.0.into());
- config.store();
+ let key_pair = (sk.0.to_vec(), pk.0.into());
+ config.key_pair = key_pair.clone();
+ std::thread::spawn(|| {
+ let mut config = CONFIG.write().unwrap();
+ config.key_pair = key_pair;
+ config.store();
+ });
}
- config.key_pair.clone()
+ *lock = Some(config.key_pair.clone());
+ return config.key_pair;
}
pub fn get_id() -> String {
@@ -618,7 +667,7 @@ impl Config {
log::info!("id updated from {} to {}", id, new_id);
}
- pub fn set_password(password: &str) {
+ pub fn set_permanent_password(password: &str) {
let mut config = CONFIG.write().unwrap();
if password == config.password {
return;
@@ -627,13 +676,8 @@ impl Config {
config.store();
}
- pub fn get_password() -> String {
- let mut password = CONFIG.read().unwrap().password.clone();
- if password.is_empty() {
- password = Config::get_auto_password();
- Config::set_password(&password);
- }
- password
+ pub fn get_permanent_password() -> String {
+ CONFIG.read().unwrap().password.clone()
}
pub fn set_salt(salt: &str) {
@@ -648,7 +692,7 @@ impl Config {
pub fn get_salt() -> String {
let mut salt = CONFIG.read().unwrap().salt.clone();
if salt.is_empty() {
- salt = Config::get_auto_password();
+ salt = Config::get_auto_password(6);
Config::set_salt(&salt);
}
salt
@@ -705,7 +749,28 @@ impl PeerConfig {
pub fn load(id: &str) -> PeerConfig {
let _ = CONFIG.read().unwrap(); // for lock
match confy::load_path(&Self::path(id)) {
- Ok(config) => config,
+ Ok(config) => {
+ let mut config: PeerConfig = config;
+ let mut store = false;
+ let (password, _, store2) =
+ decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION);
+ config.password = password;
+ store = store || store2;
+ config.options.get_mut("rdp_password").map(|v| {
+ let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION);
+ *v = password;
+ store = store || store2;
+ });
+ config.options.get_mut("os-password").map(|v| {
+ let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION);
+ *v = password;
+ store = store || store2;
+ });
+ if store {
+ config.store(id);
+ }
+ config
+ }
Err(err) => {
log::error!("Failed to load config: {}", err);
Default::default()
@@ -715,7 +780,17 @@ impl PeerConfig {
pub fn store(&self, id: &str) {
let _ = CONFIG.read().unwrap(); // for lock
- if let Err(err) = confy::store_path(Self::path(id), self) {
+ let mut config = self.clone();
+ config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION);
+ config
+ .options
+ .get_mut("rdp_password")
+ .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION));
+ config
+ .options
+ .get_mut("os-password")
+ .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION));
+ if let Err(err) = store_path(Self::path(id), config) {
log::error!("Failed to store config: {}", err);
}
}
@@ -847,10 +922,26 @@ impl LocalConfig {
}
}
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct DiscoveryPeer {
+ pub id: String,
+ #[serde(with = "serde_with::rust::map_as_tuple_list")]
+ pub ip_mac: HashMap,
+ pub username: String,
+ pub hostname: String,
+ pub platform: String,
+ pub online: bool,
+}
+
+impl DiscoveryPeer {
+ pub fn is_same_peer(&self, other: &DiscoveryPeer) -> bool {
+ self.id == other.id && self.username == other.username
+ }
+}
+
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct LanPeers {
- #[serde(default)]
- pub peers: String,
+ pub peers: Vec,
}
impl LanPeers {
@@ -865,9 +956,11 @@ impl LanPeers {
}
}
- pub fn store(peers: String) {
- let f = LanPeers { peers };
- if let Err(err) = confy::store_path(Config::file_("_lan_peers"), f) {
+ pub fn store(peers: &Vec) {
+ let f = LanPeers {
+ peers: peers.clone(),
+ };
+ if let Err(err) = store_path(Config::file_("_lan_peers"), f) {
log::error!("Failed to store lan peers: {}", err);
}
}
@@ -881,6 +974,26 @@ impl LanPeers {
}
}
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct HwCodecConfig {
+ #[serde(default)]
+ pub options: HashMap,
+}
+
+impl HwCodecConfig {
+ pub fn load() -> HwCodecConfig {
+ Config::load_::("_hwcodec")
+ }
+
+ pub fn store(&self) {
+ Config::store_(self, "_hwcodec");
+ }
+
+ pub fn remove() {
+ std::fs::remove_file(Config::file_("_hwcodec")).ok();
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs
index 4512ce940..4880b4622 100644
--- a/libs/hbb_common/src/fs.rs
+++ b/libs/hbb_common/src/fs.rs
@@ -573,7 +573,7 @@ impl TransferJob {
log::info!("file num truncated, ignoring");
} else {
match r.union {
- Some(file_transfer_send_confirm_request::Union::skip(s)) => {
+ Some(file_transfer_send_confirm_request::Union::Skip(s)) => {
if s {
log::debug!("skip file id:{}, file_num:{}", r.id, r.file_num);
self.skip_current_file();
@@ -581,7 +581,7 @@ impl TransferJob {
self.set_file_confirmed(true);
}
}
- Some(file_transfer_send_confirm_request::Union::offset_blk(_offset)) => {
+ Some(file_transfer_send_confirm_request::Union::OffsetBlk(_offset)) => {
self.set_file_confirmed(true);
}
_ => {}
diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs
index 0a9dace0c..2fdd74cd5 100644
--- a/libs/hbb_common/src/lib.rs
+++ b/libs/hbb_common/src/lib.rs
@@ -1,11 +1,12 @@
pub mod compress;
-#[path = "./protos/message.rs"]
-pub mod message_proto;
-#[path = "./protos/rendezvous.rs"]
-pub mod rendezvous_proto;
+pub mod platform;
+pub mod protos;
pub use bytes;
+use config::Config;
pub use futures;
pub use protobuf;
+pub use protos::message as message_proto;
+pub use protos::rendezvous as rendezvous_proto;
use std::{
fs::File,
io::{self, BufRead},
@@ -27,6 +28,7 @@ pub use anyhow::{self, bail};
pub use futures_util;
pub mod config;
pub mod fs;
+pub use lazy_static;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use mac_address;
pub use rand;
@@ -35,7 +37,7 @@ pub use sodiumoxide;
pub use tokio_socks;
pub use tokio_socks::IntoTargetAddr;
pub use tokio_socks::TargetAddr;
-pub use lazy_static;
+pub mod password_security;
#[cfg(feature = "quic")]
pub type Stream = quic::Connection;
@@ -200,6 +202,14 @@ pub fn get_modified_time(path: &std::path::Path) -> SystemTime {
.unwrap_or(UNIX_EPOCH)
}
+pub fn get_uuid() -> Vec {
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ if let Ok(id) = machine_uid::get() {
+ return id.into();
+ }
+ Config::get_key_pair().1
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs
new file mode 100644
index 000000000..ba57c11c4
--- /dev/null
+++ b/libs/hbb_common/src/password_security.rs
@@ -0,0 +1,218 @@
+use crate::config::Config;
+use sodiumoxide::base64;
+use std::sync::{Arc, RwLock};
+
+lazy_static::lazy_static! {
+ pub static ref TEMPORARY_PASSWORD:Arc> = Arc::new(RwLock::new(Config::get_auto_password(temporary_password_length())));
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum VerificationMethod {
+ OnlyUseTemporaryPassword,
+ OnlyUsePermanentPassword,
+ UseBothPasswords,
+}
+
+// Should only be called in server
+pub fn update_temporary_password() {
+ *TEMPORARY_PASSWORD.write().unwrap() = Config::get_auto_password(temporary_password_length());
+}
+
+// Should only be called in server
+pub fn temporary_password() -> String {
+ TEMPORARY_PASSWORD.read().unwrap().clone()
+}
+
+fn verification_method() -> VerificationMethod {
+ let method = Config::get_option("verification-method");
+ if method == "use-temporary-password" {
+ VerificationMethod::OnlyUseTemporaryPassword
+ } else if method == "use-permanent-password" {
+ VerificationMethod::OnlyUsePermanentPassword
+ } else {
+ VerificationMethod::UseBothPasswords // default
+ }
+}
+
+pub fn temporary_password_length() -> usize {
+ let length = Config::get_option("temporary-password-length");
+ if length == "8" {
+ 8
+ } else if length == "10" {
+ 10
+ } else {
+ 6 // default
+ }
+}
+
+pub fn temporary_enabled() -> bool {
+ verification_method() != VerificationMethod::OnlyUsePermanentPassword
+}
+
+pub fn permanent_enabled() -> bool {
+ verification_method() != VerificationMethod::OnlyUseTemporaryPassword
+}
+
+pub fn has_valid_password() -> bool {
+ temporary_enabled() && !temporary_password().is_empty()
+ || permanent_enabled() && !Config::get_permanent_password().is_empty()
+}
+
+const VERSION_LEN: usize = 2;
+
+pub fn encrypt_str_or_original(s: &str, version: &str) -> String {
+ if decrypt_str_or_original(s, version).1 {
+ log::error!("Duplicate encryption!");
+ return s.to_owned();
+ }
+ if version == "00" {
+ if let Ok(s) = encrypt(s.as_bytes()) {
+ return version.to_owned() + &s;
+ }
+ }
+ s.to_owned()
+}
+
+// String: password
+// bool: whether decryption is successful
+// bool: whether should store to re-encrypt when load
+pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) {
+ if s.len() > VERSION_LEN {
+ let version = &s[..VERSION_LEN];
+ if version == "00" {
+ if let Ok(v) = decrypt(&s[VERSION_LEN..].as_bytes()) {
+ return (
+ String::from_utf8_lossy(&v).to_string(),
+ true,
+ version != current_version,
+ );
+ }
+ }
+ }
+
+ (s.to_owned(), false, !s.is_empty())
+}
+
+pub fn encrypt_vec_or_original(v: &[u8], version: &str) -> Vec {
+ if decrypt_vec_or_original(v, version).1 {
+ log::error!("Duplicate encryption!");
+ return v.to_owned();
+ }
+ if version == "00" {
+ if let Ok(s) = encrypt(v) {
+ let mut version = version.to_owned().into_bytes();
+ version.append(&mut s.into_bytes());
+ return version;
+ }
+ }
+ v.to_owned()
+}
+
+// String: password
+// bool: whether decryption is successful
+// bool: whether should store to re-encrypt when load
+pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec, bool, bool) {
+ if v.len() > VERSION_LEN {
+ let version = String::from_utf8_lossy(&v[..VERSION_LEN]);
+ if version == "00" {
+ if let Ok(v) = decrypt(&v[VERSION_LEN..]) {
+ return (v, true, version != current_version);
+ }
+ }
+ }
+
+ (v.to_owned(), false, !v.is_empty())
+}
+
+fn encrypt(v: &[u8]) -> Result {
+ if v.len() > 0 {
+ symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
+ } else {
+ Err(())
+ }
+}
+
+fn decrypt(v: &[u8]) -> Result, ()> {
+ if v.len() > 0 {
+ base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false))
+ } else {
+ Err(())
+ }
+}
+
+fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result, ()> {
+ use sodiumoxide::crypto::secretbox;
+ use std::convert::TryInto;
+
+ let mut keybuf = crate::get_uuid();
+ keybuf.resize(secretbox::KEYBYTES, 0);
+ let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?);
+ let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
+
+ if encrypt {
+ Ok(secretbox::seal(data, &nonce, &key))
+ } else {
+ secretbox::open(data, &nonce, &key)
+ }
+}
+
+mod test {
+
+ #[test]
+ fn test() {
+ use super::*;
+
+ let version = "00";
+
+ println!("test str");
+ let data = "Hello World";
+ let encrypted = encrypt_str_or_original(data, version);
+ let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version);
+ println!("data: {}", data);
+ println!("encrypted: {}", encrypted);
+ println!("decrypted: {}", decrypted);
+ assert_eq!(data, decrypted);
+ assert_eq!(version, &encrypted[..2]);
+ assert_eq!(succ, true);
+ assert_eq!(store, false);
+ let (_, _, store) = decrypt_str_or_original(&encrypted, "99");
+ assert_eq!(store, true);
+ assert_eq!(decrypt_str_or_original(&decrypted, version).1, false);
+ assert_eq!(encrypt_str_or_original(&encrypted, version), encrypted);
+
+ println!("test vec");
+ let data: Vec = vec![1, 2, 3, 4, 5, 6];
+ let encrypted = encrypt_vec_or_original(&data, version);
+ let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version);
+ println!("data: {:?}", data);
+ println!("encrypted: {:?}", encrypted);
+ println!("decrypted: {:?}", decrypted);
+ assert_eq!(data, decrypted);
+ assert_eq!(version.as_bytes(), &encrypted[..2]);
+ assert_eq!(store, false);
+ assert_eq!(succ, true);
+ let (_, _, store) = decrypt_vec_or_original(&encrypted, "99");
+ assert_eq!(store, true);
+ assert_eq!(decrypt_vec_or_original(&decrypted, version).1, false);
+ assert_eq!(encrypt_vec_or_original(&encrypted, version), encrypted);
+
+ println!("test original");
+ let data = version.to_string() + "Hello World";
+ let (decrypted, succ, store) = decrypt_str_or_original(&data, version);
+ assert_eq!(data, decrypted);
+ assert_eq!(store, true);
+ assert_eq!(succ, false);
+ let verbytes = version.as_bytes();
+ let data: Vec = vec![verbytes[0] as u8, verbytes[1] as u8, 1, 2, 3, 4, 5, 6];
+ let (decrypted, succ, store) = decrypt_vec_or_original(&data, version);
+ assert_eq!(data, decrypted);
+ assert_eq!(store, true);
+ assert_eq!(succ, false);
+ let (_, succ, store) = decrypt_str_or_original("", version);
+ assert_eq!(store, false);
+ assert_eq!(succ, false);
+ let (_, succ, store) = decrypt_vec_or_original(&vec![], version);
+ assert_eq!(store, false);
+ assert_eq!(succ, false);
+ }
+}
diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs
new file mode 100644
index 000000000..865033204
--- /dev/null
+++ b/libs/hbb_common/src/platform/linux.rs
@@ -0,0 +1,111 @@
+use crate::ResultType;
+
+pub fn get_display_server() -> String {
+ let session = get_value_of_seat0(0);
+ get_display_server_of_session(&session)
+}
+
+fn get_display_server_of_session(session: &str) -> String {
+ if let Ok(output) = std::process::Command::new("loginctl")
+ .args(vec!["show-session", "-p", "Type", session])
+ .output()
+ // Check session type of the session
+ {
+ let display_server = String::from_utf8_lossy(&output.stdout)
+ .replace("Type=", "")
+ .trim_end()
+ .into();
+ if display_server == "tty" {
+ // If the type is tty...
+ if let Ok(output) = std::process::Command::new("loginctl")
+ .args(vec!["show-session", "-p", "TTY", session])
+ .output()
+ // Get the tty number
+ {
+ let tty: String = String::from_utf8_lossy(&output.stdout)
+ .replace("TTY=", "")
+ .trim_end()
+ .into();
+ if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty))
+ // And check if Xorg is running on that tty
+ {
+ if xorg_results.trim_end().to_string() != "" {
+ // If it is, manually return "x11", otherwise return tty
+ "x11".to_owned()
+ } else {
+ display_server
+ }
+ } else {
+ // If any of these commands fail just fall back to the display server
+ display_server
+ }
+ } else {
+ display_server
+ }
+ } else {
+ // loginctl has not given the expected output. try something else.
+ if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
+ return sestype.to_owned();
+ }
+ // If the session is not a tty, then just return the type as usual
+ display_server
+ }
+ } else {
+ "".to_owned()
+ }
+}
+
+pub fn get_value_of_seat0(i: usize) -> String {
+ if let Ok(output) = std::process::Command::new("loginctl").output() {
+ for line in String::from_utf8_lossy(&output.stdout).lines() {
+ if line.contains("seat0") {
+ if let Some(sid) = line.split_whitespace().nth(0) {
+ if is_active(sid) {
+ if let Some(uid) = line.split_whitespace().nth(i) {
+ return uid.to_owned();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
+ if let Ok(output) = std::process::Command::new("loginctl").output() {
+ for line in String::from_utf8_lossy(&output.stdout).lines() {
+ if let Some(sid) = line.split_whitespace().nth(0) {
+ let d = get_display_server_of_session(sid);
+ if is_active(sid) && d != "tty" {
+ if let Some(uid) = line.split_whitespace().nth(i) {
+ return uid.to_owned();
+ }
+ }
+ }
+ }
+ }
+
+ // loginctl has not given the expected output. try something else.
+ if let Ok(sid) = std::env::var("XDG_SESSION_ID") { // could also execute "cat /proc/self/sessionid"
+ return sid.to_owned();
+ }
+
+ return "".to_owned();
+}
+
+fn is_active(sid: &str) -> bool {
+ if let Ok(output) = std::process::Command::new("loginctl")
+ .args(vec!["show-session", "-p", "State", sid])
+ .output()
+ {
+ String::from_utf8_lossy(&output.stdout).contains("active")
+ } else {
+ false
+ }
+}
+
+pub fn run_cmds(cmds: String) -> ResultType {
+ let output = std::process::Command::new("sh")
+ .args(vec!["-c", &cmds])
+ .output()?;
+ Ok(String::from_utf8_lossy(&output.stdout).to_string())
+}
diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs
new file mode 100644
index 000000000..8daba257f
--- /dev/null
+++ b/libs/hbb_common/src/platform/mod.rs
@@ -0,0 +1,2 @@
+#[cfg(target_os = "linux")]
+pub mod linux;
diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs
index 4213392a5..3532dd1e0 100644
--- a/libs/hbb_common/src/udp.rs
+++ b/libs/hbb_common/src/udp.rs
@@ -27,6 +27,8 @@ fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result 0 {
socket.set_recv_buffer_size(buf_size).ok();
}
diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml
index c99ec45da..d40eb0cfd 100644
--- a/libs/scrap/Cargo.toml
+++ b/libs/scrap/Cargo.toml
@@ -18,6 +18,7 @@ cfg-if = "1.0"
libc = "0.2"
num_cpus = "1.13"
lazy_static = "1.4"
+hbb_common = { path = "../hbb_common" }
[dependencies.winapi]
version = "0.3"
@@ -48,3 +49,6 @@ tracing = { version = "0.1", optional = true }
gstreamer = { version = "0.16", optional = true }
gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
gstreamer-video = { version = "0.16", optional = true }
+
+[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
+hwcodec = { git = "https://github.com/21pages/hwcodec", optional = true }
diff --git a/libs/scrap/examples/capture_mag.rs b/libs/scrap/examples/capture_mag.rs
index 3e15b4e69..81b2d8573 100644
--- a/libs/scrap/examples/capture_mag.rs
+++ b/libs/scrap/examples/capture_mag.rs
@@ -4,7 +4,7 @@ extern crate scrap;
use std::fs::File;
#[cfg(windows)]
-use scrap::CapturerMag;
+use scrap::{CapturerMag, TraitCapturer};
use scrap::{i420_to_rgb, Display};
fn main() {
@@ -21,6 +21,8 @@ fn get_display(i: usize) -> Display {
#[cfg(windows)]
fn record(i: usize) {
+ use std::time::Duration;
+
for d in Display::all().unwrap() {
println!("{:?} {} {}", d.origin(), d.width(), d.height());
}
@@ -40,7 +42,7 @@ fn record(i: usize) {
println!("Filter window for cls {} name {}", wnd_cls, wnd_name);
}
- let frame = capture_mag.frame(0).unwrap();
+ let frame = capture_mag.frame(Duration::from_millis(0)).unwrap();
println!("Capture data len: {}, Saving...", frame.len());
let mut bitflipped = Vec::with_capacity(w * h * 4);
@@ -76,7 +78,7 @@ fn record(i: usize) {
println!("Filter window for cls {} title {}", wnd_cls, wnd_title);
}
- let buffer = capture_mag.frame(0).unwrap();
+ let buffer = capture_mag.frame(Duration::from_millis(0)).unwrap();
println!("Capture data len: {}, Saving...", buffer.len());
let mut frame = Default::default();
diff --git a/libs/scrap/examples/ffplay.rs b/libs/scrap/examples/ffplay.rs
index a4ca1b35b..b8fe16e35 100644
--- a/libs/scrap/examples/ffplay.rs
+++ b/libs/scrap/examples/ffplay.rs
@@ -1,7 +1,9 @@
+use std::time::Duration;
+
extern crate scrap;
fn main() {
- use scrap::{Capturer, Display};
+ use scrap::{Capturer, Display, TraitCapturer};
use std::io::ErrorKind::WouldBlock;
use std::io::Write;
use std::process::{Command, Stdio};
@@ -29,7 +31,7 @@ fn main() {
let mut out = child.stdin.unwrap();
loop {
- match capturer.frame(0) {
+ match capturer.frame(Duration::from_millis(0)) {
Ok(frame) => {
// Write the frame, removing end-of-row padding.
let stride = frame.len() / h;
diff --git a/libs/scrap/examples/record-screen.rs b/libs/scrap/examples/record-screen.rs
index 2a56c0dcd..5df97838e 100644
--- a/libs/scrap/examples/record-screen.rs
+++ b/libs/scrap/examples/record-screen.rs
@@ -13,11 +13,12 @@ use std::time::{Duration, Instant};
use std::{io, thread};
use docopt::Docopt;
+use scrap::codec::{EncoderApi, EncoderCfg};
use webm::mux;
use webm::mux::Track;
-use scrap::codec as vpx_encode;
-use scrap::{Capturer, Display, STRIDE_ALIGN};
+use scrap::vpxcodec as vpx_encode;
+use scrap::{TraitCapturer, Capturer, Display, STRIDE_ALIGN};
const USAGE: &'static str = "
Simple WebM screen capture.
@@ -89,27 +90,22 @@ fn main() -> io::Result<()> {
mux::Segment::new(mux::Writer::new(out)).expect("Could not initialize the multiplexer.");
let (vpx_codec, mux_codec) = match args.flag_codec {
- Codec::Vp8 => (vpx_encode::VideoCodecId::VP8, mux::VideoCodecId::VP8),
- Codec::Vp9 => (vpx_encode::VideoCodecId::VP9, mux::VideoCodecId::VP9),
+ Codec::Vp8 => (vpx_encode::VpxVideoCodecId::VP8, mux::VideoCodecId::VP8),
+ Codec::Vp9 => (vpx_encode::VpxVideoCodecId::VP9, mux::VideoCodecId::VP9),
};
let mut vt = webm.add_video_track(width, height, None, mux_codec);
// Setup the encoder.
- let mut vpx = vpx_encode::Encoder::new(
- &vpx_encode::Config {
- width,
- height,
- timebase: [1, 1000],
- bitrate: args.flag_bv,
- codec: vpx_codec,
- rc_min_quantizer: 0,
- rc_max_quantizer: 0,
- speed: 6,
- },
- 0,
- )
+ let mut vpx = vpx_encode::VpxEncoder::new(EncoderCfg::VPX(vpx_encode::VpxEncoderConfig {
+ width,
+ height,
+ timebase: [1, 1000],
+ bitrate: args.flag_bv,
+ codec: vpx_codec,
+ num_threads: 0,
+ }))
.unwrap();
// Start recording.
@@ -138,7 +134,7 @@ fn main() -> io::Result<()> {
break;
}
- if let Ok(frame) = c.frame(0) {
+ if let Ok(frame) = c.frame(Duration::from_millis(0)) {
let ms = time.as_secs() * 1000 + time.subsec_millis() as u64;
for frame in vpx.encode(ms as i64, &frame, STRIDE_ALIGN).unwrap() {
diff --git a/libs/scrap/examples/screenshot.rs b/libs/scrap/examples/screenshot.rs
index b52ea11f7..636768eb8 100644
--- a/libs/scrap/examples/screenshot.rs
+++ b/libs/scrap/examples/screenshot.rs
@@ -6,7 +6,7 @@ use std::io::ErrorKind::WouldBlock;
use std::thread;
use std::time::Duration;
-use scrap::{i420_to_rgb, Capturer, Display};
+use scrap::{i420_to_rgb, Capturer, Display, TraitCapturer};
fn main() {
let n = Display::all().unwrap().len();
@@ -34,7 +34,7 @@ fn record(i: usize) {
loop {
// Wait until there's a frame.
- let buffer = match capturer.frame(0) {
+ let buffer = match capturer.frame(Duration::from_millis(0)) {
Ok(buffer) => buffer,
Err(error) => {
if error.kind() == WouldBlock {
@@ -83,7 +83,7 @@ fn record(i: usize) {
loop {
// Wait until there's a frame.
- let buffer = match capturer.frame(0) {
+ let buffer = match capturer.frame(Duration::from_millis(0)) {
Ok(buffer) => buffer,
Err(error) => {
if error.kind() == WouldBlock {
diff --git a/libs/scrap/src/common/android.rs b/libs/scrap/src/common/android.rs
index 1975a6505..949188712 100644
--- a/libs/scrap/src/common/android.rs
+++ b/libs/scrap/src/common/android.rs
@@ -3,8 +3,8 @@ use crate::rgba_to_i420;
use lazy_static::lazy_static;
use serde_json::Value;
use std::collections::HashMap;
-use std::io;
use std::sync::Mutex;
+use std::{io, time::Duration};
lazy_static! {
static ref SCREEN_SIZE: Mutex<(u16, u16, u16)> = Mutex::new((0, 0, 0)); // (width, height, scale)
@@ -32,8 +32,12 @@ impl Capturer {
pub fn height(&self) -> usize {
self.display.height() as usize
}
+}
- pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result> {
+impl crate::TraitCapturer for Capturer {
+ fn set_use_yuv(&mut self, _use_yuv: bool) {}
+
+ fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result> {
if let Some(buf) = get_video_raw() {
crate::would_block_if_equal(&mut self.saved_raw_data, buf)?;
rgba_to_i420(self.width(), self.height(), buf, &mut self.bgra);
diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs
index f1533d7cf..f0bd1c5f7 100644
--- a/libs/scrap/src/common/codec.rs
+++ b/libs/scrap/src/common/codec.rs
@@ -1,536 +1,372 @@
-// https://github.com/astraw/vpx-encode
-// https://github.com/astraw/env-libvpx-sys
-// https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs
-
-use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *};
-use std::os::raw::{c_int, c_uint};
-use std::{ptr, slice};
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
-pub enum VideoCodecId {
- VP8,
- VP9,
-}
-
-impl Default for VideoCodecId {
- fn default() -> VideoCodecId {
- VideoCodecId::VP9
- }
-}
-
-pub struct Encoder {
- ctx: vpx_codec_ctx_t,
- width: usize,
- height: usize,
-}
-
-pub struct Decoder {
- ctx: vpx_codec_ctx_t,
-}
-
-#[derive(Debug)]
-pub enum Error {
- FailedCall(String),
- BadPtr(String),
-}
-
-impl std::fmt::Display for Error {
- fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
- write!(f, "{:?}", self)
- }
-}
-
-impl std::error::Error for Error {}
-
-pub type Result = std::result::Result;
-
-macro_rules! call_vpx {
- ($x:expr) => {{
- let result = unsafe { $x }; // original expression
- let result_int = unsafe { std::mem::transmute::<_, i32>(result) };
- if result_int != 0 {
- return Err(Error::FailedCall(format!(
- "errcode={} {}:{}:{}:{}",
- result_int,
- module_path!(),
- file!(),
- line!(),
- column!()
- ))
- .into());
- }
- result
- }};
-}
-
-macro_rules! call_vpx_ptr {
- ($x:expr) => {{
- let result = unsafe { $x }; // original expression
- let result_int = unsafe { std::mem::transmute::<_, isize>(result) };
- if result_int == 0 {
- return Err(Error::BadPtr(format!(
- "errcode={} {}:{}:{}:{}",
- result_int,
- module_path!(),
- file!(),
- line!(),
- column!()
- ))
- .into());
- }
- result
- }};
-}
-
-impl Encoder {
- pub fn new(config: &Config, num_threads: u32) -> Result {
- let i;
- if cfg!(feature = "VP8") {
- i = match config.codec {
- VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()),
- VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()),
- };
- } else {
- i = call_vpx_ptr!(vpx_codec_vp9_cx());
- }
- let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
- call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0));
-
- // https://www.webmproject.org/docs/encoder-parameters/
- // default: c.rc_min_quantizer = 0, c.rc_max_quantizer = 63
- // try rc_resize_allowed later
-
- c.g_w = config.width;
- c.g_h = config.height;
- c.g_timebase.num = config.timebase[0];
- c.g_timebase.den = config.timebase[1];
- c.rc_target_bitrate = config.bitrate;
- c.rc_undershoot_pct = 95;
- c.rc_dropframe_thresh = 25;
- if config.rc_min_quantizer > 0 {
- c.rc_min_quantizer = config.rc_min_quantizer;
- }
- if config.rc_max_quantizer > 0 {
- c.rc_max_quantizer = config.rc_max_quantizer;
- }
- let mut speed = config.speed;
- if speed <= 0 {
- speed = 6;
- }
-
- c.g_threads = if num_threads == 0 {
- num_cpus::get() as _
- } else {
- num_threads
- };
- c.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
- // https://developers.google.com/media/vp9/bitrate-modes/
- // Constant Bitrate mode (CBR) is recommended for live streaming with VP9.
- c.rc_end_usage = vpx_rc_mode::VPX_CBR;
- // c.kf_min_dist = 0;
- // c.kf_max_dist = 999999;
- c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot
-
- /*
- VPX encoder支持two-pass encode,这是为了rate control的。
- 对于两遍编码,就是需要整个编码过程做两次,第一次会得到一些新的控制参数来进行第二遍的编码,
- 这样可以在相同的bitrate下得到最好的PSNR
- */
-
- let mut ctx = Default::default();
- call_vpx!(vpx_codec_enc_init_ver(
- &mut ctx,
- i,
- &c,
- 0,
- VPX_ENCODER_ABI_VERSION as _
- ));
-
- if config.codec == VideoCodecId::VP9 {
- // set encoder internal speed settings
- // in ffmpeg, it is --speed option
- /*
- set to 0 or a positive value 1-16, the codec will try to adapt its
- complexity depending on the time it spends encoding. Increasing this
- number will make the speed go up and the quality go down.
- Negative values mean strict enforcement of this
- while positive values are adaptive
- */
- /* https://developers.google.com/media/vp9/live-encoding
- Speed 5 to 8 should be used for live / real-time encoding.
- Lower numbers (5 or 6) are higher quality but require more CPU power.
- Higher numbers (7 or 8) will be lower quality but more manageable for lower latency
- use cases and also for lower CPU power devices such as mobile.
- */
- call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, speed,));
- // set row level multi-threading
- /*
- as some people in comments and below have already commented,
- more recent versions of libvpx support -row-mt 1 to enable tile row
- multi-threading. This can increase the number of tiles by up to 4x in VP9
- (since the max number of tile rows is 4, regardless of video height).
- To enable this, use -tile-rows N where N is the number of tile rows in
- log2 units (so -tile-rows 1 means 2 tile rows and -tile-rows 2 means 4 tile
- rows). The total number of active threads will then be equal to
- $tile_rows * $tile_columns
- */
- call_vpx!(vpx_codec_control_(
- &mut ctx,
- VP9E_SET_ROW_MT as _,
- 1 as c_int
- ));
-
- call_vpx!(vpx_codec_control_(
- &mut ctx,
- VP9E_SET_TILE_COLUMNS as _,
- 4 as c_int
- ));
- }
-
- Ok(Self {
- ctx,
- width: config.width as _,
- height: config.height as _,
- })
- }
-
- pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result {
- assert!(2 * data.len() >= 3 * self.width * self.height);
-
- let mut image = Default::default();
- call_vpx_ptr!(vpx_img_wrap(
- &mut image,
- vpx_img_fmt::VPX_IMG_FMT_I420,
- self.width as _,
- self.height as _,
- stride_align as _,
- data.as_ptr() as _,
- ));
-
- call_vpx!(vpx_codec_encode(
- &mut self.ctx,
- &image,
- pts as _,
- 1, // Duration
- 0, // Flags
- VPX_DL_REALTIME as _,
- ));
-
- Ok(EncodeFrames {
- ctx: &mut self.ctx,
- iter: ptr::null(),
- })
- }
-
- /// Notify the encoder to return any pending packets
- pub fn flush(&mut self) -> Result {
- call_vpx!(vpx_codec_encode(
- &mut self.ctx,
- ptr::null(),
- -1, // PTS
- 1, // Duration
- 0, // Flags
- VPX_DL_REALTIME as _,
- ));
-
- Ok(EncodeFrames {
- ctx: &mut self.ctx,
- iter: ptr::null(),
- })
- }
-}
-
-impl Drop for Encoder {
- fn drop(&mut self) {
- unsafe {
- let result = vpx_codec_destroy(&mut self.ctx);
- if result != VPX_CODEC_OK {
- panic!("failed to destroy vpx codec");
- }
- }
- }
-}
-
-#[derive(Clone, Copy, Debug)]
-pub struct EncodeFrame<'a> {
- /// Compressed data.
- pub data: &'a [u8],
- /// Whether the frame is a keyframe.
- pub key: bool,
- /// Presentation timestamp (in timebase units).
- pub pts: i64,
-}
-
-#[derive(Clone, Copy, Debug)]
-pub struct Config {
- /// The width (in pixels).
- pub width: c_uint,
- /// The height (in pixels).
- pub height: c_uint,
- /// The timebase numerator and denominator (in seconds).
- pub timebase: [c_int; 2],
- /// The target bitrate (in kilobits per second).
- pub bitrate: c_uint,
- /// The codec
- pub codec: VideoCodecId,
- pub rc_min_quantizer: u32,
- pub rc_max_quantizer: u32,
- pub speed: i32,
-}
-
-pub struct EncodeFrames<'a> {
- ctx: &'a mut vpx_codec_ctx_t,
- iter: vpx_codec_iter_t,
-}
-
-impl<'a> Iterator for EncodeFrames<'a> {
- type Item = EncodeFrame<'a>;
- fn next(&mut self) -> Option {
- loop {
- unsafe {
- let pkt = vpx_codec_get_cx_data(self.ctx, &mut self.iter);
- if pkt.is_null() {
- return None;
- } else if (*pkt).kind == vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT {
- let f = &(*pkt).data.frame;
- return Some(Self::Item {
- data: slice::from_raw_parts(f.buf as _, f.sz as _),
- key: (f.flags & VPX_FRAME_IS_KEY) != 0,
- pts: f.pts,
- });
- } else {
- // Ignore the packet.
- }
- }
- }
- }
-}
-
-impl Decoder {
- /// Create a new decoder
- ///
- /// # Errors
- ///
- /// The function may fail if the underlying libvpx does not provide
- /// the VP9 decoder.
- pub fn new(codec: VideoCodecId, num_threads: u32) -> Result {
- // This is sound because `vpx_codec_ctx` is a repr(C) struct without any field that can
- // cause UB if uninitialized.
- let i;
- if cfg!(feature = "VP8") {
- i = match codec {
- VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()),
- VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()),
- };
- } else {
- i = call_vpx_ptr!(vpx_codec_vp9_dx());
- }
- let mut ctx = Default::default();
- let cfg = vpx_codec_dec_cfg_t {
- threads: if num_threads == 0 {
- num_cpus::get() as _
- } else {
- num_threads
- },
- w: 0,
- h: 0,
- };
- /*
- unsafe {
- println!("{}", vpx_codec_get_caps(i));
- }
- */
- call_vpx!(vpx_codec_dec_init_ver(
- &mut ctx,
- i,
- &cfg,
- 0,
- VPX_DECODER_ABI_VERSION as _,
- ));
- Ok(Self { ctx })
- }
-
- pub fn decode2rgb(&mut self, data: &[u8], rgba: bool) -> Result> {
- let mut img = Image::new();
- for frame in self.decode(data)? {
- drop(img);
- img = frame;
- }
- for frame in self.flush()? {
- drop(img);
- img = frame;
- }
- if img.is_null() {
- Ok(Vec::new())
- } else {
- let mut out = Default::default();
- img.rgb(1, rgba, &mut out);
- Ok(out)
- }
- }
-
- /// Feed some compressed data to the encoder
- ///
- /// The `data` slice is sent to the decoder
- ///
- /// It matches a call to `vpx_codec_decode`.
- pub fn decode(&mut self, data: &[u8]) -> Result {
- call_vpx!(vpx_codec_decode(
- &mut self.ctx,
- data.as_ptr(),
- data.len() as _,
- ptr::null_mut(),
- 0,
- ));
-
- Ok(DecodeFrames {
- ctx: &mut self.ctx,
- iter: ptr::null(),
- })
- }
-
- /// Notify the decoder to return any pending frame
- pub fn flush(&mut self) -> Result {
- call_vpx!(vpx_codec_decode(
- &mut self.ctx,
- ptr::null(),
- 0,
- ptr::null_mut(),
- 0
- ));
- Ok(DecodeFrames {
- ctx: &mut self.ctx,
- iter: ptr::null(),
- })
- }
-}
-
-impl Drop for Decoder {
- fn drop(&mut self) {
- unsafe {
- let result = vpx_codec_destroy(&mut self.ctx);
- if result != VPX_CODEC_OK {
- panic!("failed to destroy vpx codec");
- }
- }
- }
-}
-
-pub struct DecodeFrames<'a> {
- ctx: &'a mut vpx_codec_ctx_t,
- iter: vpx_codec_iter_t,
-}
-
-impl<'a> Iterator for DecodeFrames<'a> {
- type Item = Image;
- fn next(&mut self) -> Option {
- let img = unsafe { vpx_codec_get_frame(self.ctx, &mut self.iter) };
- if img.is_null() {
- return None;
- } else {
- return Some(Image(img));
- }
- }
-}
-
-// https://chromium.googlesource.com/webm/libvpx/+/bali/vpx/src/vpx_image.c
-pub struct Image(*mut vpx_image_t);
-impl Image {
- #[inline]
- pub fn new() -> Self {
- Self(std::ptr::null_mut())
- }
-
- #[inline]
- pub fn is_null(&self) -> bool {
- self.0.is_null()
- }
-
- #[inline]
- pub fn width(&self) -> usize {
- self.inner().d_w as _
- }
-
- #[inline]
- pub fn height(&self) -> usize {
- self.inner().d_h as _
- }
-
- #[inline]
- pub fn format(&self) -> vpx_img_fmt_t {
- // VPX_IMG_FMT_I420
- self.inner().fmt
- }
-
- #[inline]
- pub fn inner(&self) -> &vpx_image_t {
- unsafe { &*self.0 }
- }
-
- #[inline]
- pub fn stride(&self, iplane: usize) -> i32 {
- self.inner().stride[iplane]
- }
-
- pub fn rgb(&self, stride_align: usize, rgba: bool, dst: &mut Vec) {
- let h = self.height();
- let mut w = self.width();
- let bps = if rgba { 4 } else { 3 };
- w = (w + stride_align - 1) & !(stride_align - 1);
- dst.resize(h * w * bps, 0);
- let img = self.inner();
- unsafe {
- if rgba {
- super::I420ToARGB(
- img.planes[0],
- img.stride[0],
- img.planes[1],
- img.stride[1],
- img.planes[2],
- img.stride[2],
- dst.as_mut_ptr(),
- (w * bps) as _,
- self.width() as _,
- self.height() as _,
- );
- } else {
- super::I420ToRAW(
- img.planes[0],
- img.stride[0],
- img.planes[1],
- img.stride[1],
- img.planes[2],
- img.stride[2],
- dst.as_mut_ptr(),
- (w * bps) as _,
- self.width() as _,
- self.height() as _,
- );
- }
- }
- }
-
- #[inline]
- pub fn data(&self) -> (&[u8], &[u8], &[u8]) {
- unsafe {
- let img = self.inner();
- let h = (img.d_h as usize + 1) & !1;
- let n = img.stride[0] as usize * h;
- let y = slice::from_raw_parts(img.planes[0], n);
- let n = img.stride[1] as usize * (h >> 1);
- let u = slice::from_raw_parts(img.planes[1], n);
- let v = slice::from_raw_parts(img.planes[2], n);
- (y, u, v)
- }
- }
-}
-
-impl Drop for Image {
- fn drop(&mut self) {
- if !self.0.is_null() {
- unsafe { vpx_img_free(self.0) };
- }
- }
-}
-
-unsafe impl Send for vpx_codec_ctx_t {}
+use std::ops::{Deref, DerefMut};
+#[cfg(feature = "hwcodec")]
+use std::{
+ collections::HashMap,
+ sync::{Arc, Mutex},
+};
+
+#[cfg(feature = "hwcodec")]
+use crate::hwcodec::*;
+use crate::vpxcodec::*;
+
+use hbb_common::{
+ anyhow::anyhow,
+ log,
+ message_proto::{video_frame, EncodedVideoFrames, Message, VideoCodecState},
+ ResultType,
+};
+#[cfg(feature = "hwcodec")]
+use hbb_common::{
+ config::{Config2, PeerConfig},
+ lazy_static,
+ message_proto::video_codec_state::PerferCodec,
+};
+
+#[cfg(feature = "hwcodec")]
+lazy_static::lazy_static! {
+ static ref PEER_DECODER_STATES: Arc>> = Default::default();
+}
+const SCORE_VPX: i32 = 90;
+
+#[derive(Debug, Clone)]
+pub struct HwEncoderConfig {
+ pub codec_name: String,
+ pub width: usize,
+ pub height: usize,
+ pub bitrate: i32,
+}
+
+#[derive(Debug, Clone)]
+pub enum EncoderCfg {
+ VPX(VpxEncoderConfig),
+ HW(HwEncoderConfig),
+}
+
+pub trait EncoderApi {
+ fn new(cfg: EncoderCfg) -> ResultType
+ where
+ Self: Sized;
+
+ fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType;
+
+ fn use_yuv(&self) -> bool;
+
+ fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()>;
+}
+
+pub struct DecoderCfg {
+ pub vpx: VpxDecoderConfig,
+}
+
+pub struct Encoder {
+ pub codec: Box,
+}
+
+impl Deref for Encoder {
+ type Target = Box;
+
+ fn deref(&self) -> &Self::Target {
+ &self.codec
+ }
+}
+
+impl DerefMut for Encoder {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.codec
+ }
+}
+
+pub struct Decoder {
+ vpx: VpxDecoder,
+ #[cfg(feature = "hwcodec")]
+ hw: HwDecoders,
+ #[cfg(feature = "hwcodec")]
+ i420: Vec,
+}
+
+#[derive(Debug, Clone)]
+pub enum EncoderUpdate {
+ State(VideoCodecState),
+ Remove,
+ DisableHwIfNotExist,
+}
+
+impl Encoder {
+ pub fn new(config: EncoderCfg) -> ResultType {
+ log::info!("new encoder:{:?}", config);
+ match config {
+ EncoderCfg::VPX(_) => Ok(Encoder {
+ codec: Box::new(VpxEncoder::new(config)?),
+ }),
+
+ #[cfg(feature = "hwcodec")]
+ EncoderCfg::HW(_) => match HwEncoder::new(config) {
+ Ok(hw) => Ok(Encoder {
+ codec: Box::new(hw),
+ }),
+ Err(e) => {
+ check_config_process(true);
+ Err(e)
+ }
+ },
+ #[cfg(not(feature = "hwcodec"))]
+ _ => Err(anyhow!("unsupported encoder type")),
+ }
+ }
+
+ // TODO
+ pub fn update_video_encoder(id: i32, update: EncoderUpdate) {
+ #[cfg(feature = "hwcodec")]
+ {
+ let mut states = PEER_DECODER_STATES.lock().unwrap();
+ match update {
+ EncoderUpdate::State(state) => {
+ states.insert(id, state);
+ }
+ EncoderUpdate::Remove => {
+ states.remove(&id);
+ }
+ EncoderUpdate::DisableHwIfNotExist => {
+ if !states.contains_key(&id) {
+ states.insert(id, VideoCodecState::default());
+ }
+ }
+ }
+ let name = HwEncoder::current_name();
+ if states.len() > 0 {
+ let best = HwEncoder::best();
+ let enabled_h264 = best.h264.is_some()
+ && states.len() > 0
+ && states.iter().all(|(_, s)| s.score_h264 > 0);
+ let enabled_h265 = best.h265.is_some()
+ && states.len() > 0
+ && states.iter().all(|(_, s)| s.score_h265 > 0);
+
+ // Preference first
+ let mut preference = PerferCodec::Auto;
+ let preferences: Vec<_> = states
+ .iter()
+ .filter(|(_, s)| {
+ s.perfer == PerferCodec::VPX.into()
+ || s.perfer == PerferCodec::H264.into() && enabled_h264
+ || s.perfer == PerferCodec::H265.into() && enabled_h265
+ })
+ .map(|(_, s)| s.perfer)
+ .collect();
+ if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) {
+ preference = preferences[0].enum_value_or(PerferCodec::Auto);
+ }
+
+ match preference {
+ PerferCodec::VPX => *name.lock().unwrap() = None,
+ PerferCodec::H264 => {
+ *name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name))
+ }
+ PerferCodec::H265 => {
+ *name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name))
+ }
+ PerferCodec::Auto => {
+ // score encoder
+ let mut score_vpx = SCORE_VPX;
+ let mut score_h264 = best.h264.as_ref().map_or(0, |c| c.score);
+ let mut score_h265 = best.h265.as_ref().map_or(0, |c| c.score);
+
+ // score decoder
+ score_vpx += states.iter().map(|s| s.1.score_vpx).sum::();
+ if enabled_h264 {
+ score_h264 += states.iter().map(|s| s.1.score_h264).sum::();
+ }
+ if enabled_h265 {
+ score_h265 += states.iter().map(|s| s.1.score_h265).sum::();
+ }
+
+ if enabled_h265 && score_h265 >= score_vpx && score_h265 >= score_h264 {
+ *name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name));
+ } else if enabled_h264
+ && score_h264 >= score_vpx
+ && score_h264 >= score_h265
+ {
+ *name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name));
+ } else {
+ *name.lock().unwrap() = None;
+ }
+ }
+ }
+
+ log::info!(
+ "connection count:{}, used preference:{:?}, encoder:{:?}",
+ states.len(),
+ preference,
+ name.lock().unwrap()
+ )
+ } else {
+ *name.lock().unwrap() = None;
+ }
+ }
+ #[cfg(not(feature = "hwcodec"))]
+ {
+ let _ = id;
+ let _ = update;
+ }
+ }
+ #[inline]
+ pub fn current_hw_encoder_name() -> Option {
+ #[cfg(feature = "hwcodec")]
+ if check_hwcodec_config() {
+ return HwEncoder::current_name().lock().unwrap().clone();
+ } else {
+ return None;
+ }
+ #[cfg(not(feature = "hwcodec"))]
+ return None;
+ }
+
+ pub fn supported_encoding() -> (bool, bool) {
+ #[cfg(feature = "hwcodec")]
+ if check_hwcodec_config() {
+ let best = HwEncoder::best();
+ (
+ best.h264.as_ref().map_or(false, |c| c.score > 0),
+ best.h265.as_ref().map_or(false, |c| c.score > 0),
+ )
+ } else {
+ (false, false)
+ }
+ #[cfg(not(feature = "hwcodec"))]
+ (false, false)
+ }
+}
+
+impl Decoder {
+ pub fn video_codec_state(_id: &str) -> VideoCodecState {
+ #[cfg(feature = "hwcodec")]
+ if check_hwcodec_config() {
+ let best = HwDecoder::best();
+ VideoCodecState {
+ score_vpx: SCORE_VPX,
+ score_h264: best.h264.map_or(0, |c| c.score),
+ score_h265: best.h265.map_or(0, |c| c.score),
+ perfer: Self::codec_preference(_id).into(),
+ ..Default::default()
+ }
+ } else {
+ return VideoCodecState {
+ score_vpx: SCORE_VPX,
+ ..Default::default()
+ };
+ }
+ #[cfg(not(feature = "hwcodec"))]
+ VideoCodecState {
+ score_vpx: SCORE_VPX,
+ ..Default::default()
+ }
+ }
+
+ pub fn new(config: DecoderCfg) -> Decoder {
+ let vpx = VpxDecoder::new(config.vpx).unwrap();
+ Decoder {
+ vpx,
+ #[cfg(feature = "hwcodec")]
+ hw: HwDecoder::new_decoders(),
+ #[cfg(feature = "hwcodec")]
+ i420: vec![],
+ }
+ }
+
+ pub fn handle_video_frame(
+ &mut self,
+ frame: &video_frame::Union,
+ rgb: &mut Vec,
+ ) -> ResultType {
+ match frame {
+ video_frame::Union::Vp9s(vp9s) => {
+ Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, rgb)
+ }
+ #[cfg(feature = "hwcodec")]
+ video_frame::Union::H264s(h264s) => {
+ if let Some(decoder) = &mut self.hw.h264 {
+ Decoder::handle_hw_video_frame(decoder, h264s, rgb, &mut self.i420)
+ } else {
+ Err(anyhow!("don't support h264!"))
+ }
+ }
+ #[cfg(feature = "hwcodec")]
+ video_frame::Union::H265s(h265s) => {
+ if let Some(decoder) = &mut self.hw.h265 {
+ Decoder::handle_hw_video_frame(decoder, h265s, rgb, &mut self.i420)
+ } else {
+ Err(anyhow!("don't support h265!"))
+ }
+ }
+ _ => Err(anyhow!("unsupported video frame type!")),
+ }
+ }
+
+ fn handle_vp9s_video_frame(
+ decoder: &mut VpxDecoder,
+ vp9s: &EncodedVideoFrames,
+ rgb: &mut Vec,
+ ) -> ResultType {
+ let mut last_frame = Image::new();
+ for vp9 in vp9s.frames.iter() {
+ for frame in decoder.decode(&vp9.data)? {
+ drop(last_frame);
+ last_frame = frame;
+ }
+ }
+ for frame in decoder.flush()? {
+ drop(last_frame);
+ last_frame = frame;
+ }
+ if last_frame.is_null() {
+ Ok(false)
+ } else {
+ last_frame.rgb(1, true, rgb);
+ Ok(true)
+ }
+ }
+
+ #[cfg(feature = "hwcodec")]
+ fn handle_hw_video_frame(
+ decoder: &mut HwDecoder,
+ frames: &EncodedVideoFrames,
+ rgb: &mut Vec,
+ i420: &mut Vec,
+ ) -> ResultType {
+ let mut ret = false;
+ for h264 in frames.frames.iter() {
+ for image in decoder.decode(&h264.data)? {
+ // TODO: just process the last frame
+ if image.bgra(rgb, i420).is_ok() {
+ ret = true;
+ }
+ }
+ }
+ return Ok(ret);
+ }
+
+ #[cfg(feature = "hwcodec")]
+ fn codec_preference(id: &str) -> PerferCodec {
+ let codec = PeerConfig::load(id)
+ .options
+ .get("codec-preference")
+ .map_or("".to_owned(), |c| c.to_owned());
+ if codec == "vp9" {
+ PerferCodec::VPX
+ } else if codec == "h264" {
+ PerferCodec::H264
+ } else if codec == "h265" {
+ PerferCodec::H265
+ } else {
+ PerferCodec::Auto
+ }
+ }
+}
+
+#[cfg(feature = "hwcodec")]
+fn check_hwcodec_config() -> bool {
+ if let Some(v) = Config2::get().options.get("enable-hwcodec") {
+ return v != "N";
+ }
+ return true; // default is true
+}
diff --git a/libs/scrap/src/common/convert.rs b/libs/scrap/src/common/convert.rs
index 1e5f2164d..2b0223a0a 100644
--- a/libs/scrap/src/common/convert.rs
+++ b/libs/scrap/src/common/convert.rs
@@ -49,6 +49,17 @@ extern "C" {
height: c_int,
) -> c_int;
+ pub fn ARGBToNV12(
+ src_bgra: *const u8,
+ src_stride_bgra: c_int,
+ dst_y: *mut u8,
+ dst_stride_y: c_int,
+ dst_uv: *mut u8,
+ dst_stride_uv: c_int,
+ width: c_int,
+ height: c_int,
+ ) -> c_int;
+
pub fn NV12ToI420(
src_y: *const u8,
src_stride_y: c_int,
@@ -91,6 +102,17 @@ extern "C" {
width: c_int,
height: c_int,
) -> c_int;
+
+ pub fn NV12ToARGB(
+ src_y: *const u8,
+ src_stride_y: c_int,
+ src_uv: *const u8,
+ src_stride_uv: c_int,
+ dst_rgba: *mut u8,
+ dst_stride_rgba: c_int,
+ width: c_int,
+ height: c_int,
+ ) -> c_int;
}
// https://github.com/webmproject/libvpx/blob/master/vpx/src/vpx_image.c
@@ -220,3 +242,195 @@ pub unsafe fn nv12_to_i420(
height as _,
);
}
+
+#[cfg(feature = "hwcodec")]
+pub mod hw {
+ use hbb_common::{anyhow::anyhow, ResultType};
+ #[cfg(target_os = "windows")]
+ use hwcodec::{ffmpeg::ffmpeg_linesize_offset_length, AVPixelFormat};
+
+ pub fn hw_bgra_to_i420(
+ width: usize,
+ height: usize,
+ stride: &[i32],
+ offset: &[i32],
+ length: i32,
+ src: &[u8],
+ dst: &mut Vec,
+ ) {
+ let stride_y = stride[0] as usize;
+ let stride_u = stride[1] as usize;
+ let stride_v = stride[2] as usize;
+ let offset_u = offset[0] as usize;
+ let offset_v = offset[1] as usize;
+
+ dst.resize(length as _, 0);
+ let dst_y = dst.as_mut_ptr();
+ let dst_u = dst[offset_u..].as_mut_ptr();
+ let dst_v = dst[offset_v..].as_mut_ptr();
+ unsafe {
+ super::ARGBToI420(
+ src.as_ptr(),
+ (src.len() / height) as _,
+ dst_y,
+ stride_y as _,
+ dst_u,
+ stride_u as _,
+ dst_v,
+ stride_v as _,
+ width as _,
+ height as _,
+ );
+ }
+ }
+
+ pub fn hw_bgra_to_nv12(
+ width: usize,
+ height: usize,
+ stride: &[i32],
+ offset: &[i32],
+ length: i32,
+ src: &[u8],
+ dst: &mut Vec,
+ ) {
+ let stride_y = stride[0] as usize;
+ let stride_uv = stride[1] as usize;
+ let offset_uv = offset[0] as usize;
+
+ dst.resize(length as _, 0);
+ let dst_y = dst.as_mut_ptr();
+ let dst_uv = dst[offset_uv..].as_mut_ptr();
+ unsafe {
+ super::ARGBToNV12(
+ src.as_ptr(),
+ (src.len() / height) as _,
+ dst_y,
+ stride_y as _,
+ dst_uv,
+ stride_uv as _,
+ width as _,
+ height as _,
+ );
+ }
+ }
+
+ #[cfg(target_os = "windows")]
+ pub fn hw_nv12_to_bgra(
+ width: usize,
+ height: usize,
+ src_y: &[u8],
+ src_uv: &[u8],
+ src_stride_y: usize,
+ src_stride_uv: usize,
+ dst: &mut Vec,
+ i420: &mut Vec,
+ align: usize,
+ ) -> ResultType<()> {
+ let nv12_stride_y = src_stride_y;
+ let nv12_stride_uv = src_stride_uv;
+ if let Ok((linesize_i420, offset_i420, i420_len)) =
+ ffmpeg_linesize_offset_length(AVPixelFormat::AV_PIX_FMT_YUV420P, width, height, align)
+ {
+ dst.resize(width * height * 4, 0);
+ let i420_stride_y = linesize_i420[0];
+ let i420_stride_u = linesize_i420[1];
+ let i420_stride_v = linesize_i420[2];
+ i420.resize(i420_len as _, 0);
+
+ unsafe {
+ let i420_offset_y = i420.as_ptr().add(0) as _;
+ let i420_offset_u = i420.as_ptr().add(offset_i420[0] as _) as _;
+ let i420_offset_v = i420.as_ptr().add(offset_i420[1] as _) as _;
+ super::NV12ToI420(
+ src_y.as_ptr(),
+ nv12_stride_y as _,
+ src_uv.as_ptr(),
+ nv12_stride_uv as _,
+ i420_offset_y,
+ i420_stride_y,
+ i420_offset_u,
+ i420_stride_u,
+ i420_offset_v,
+ i420_stride_v,
+ width as _,
+ height as _,
+ );
+ super::I420ToARGB(
+ i420_offset_y,
+ i420_stride_y,
+ i420_offset_u,
+ i420_stride_u,
+ i420_offset_v,
+ i420_stride_v,
+ dst.as_mut_ptr(),
+ (width * 4) as _,
+ width as _,
+ height as _,
+ );
+ return Ok(());
+ };
+ }
+ return Err(anyhow!("get linesize offset failed"));
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ pub fn hw_nv12_to_bgra(
+ width: usize,
+ height: usize,
+ src_y: &[u8],
+ src_uv: &[u8],
+ src_stride_y: usize,
+ src_stride_uv: usize,
+ dst: &mut Vec,
+ _i420: &mut Vec,
+ _align: usize,
+ ) -> ResultType<()> {
+ dst.resize(width * height * 4, 0);
+ unsafe {
+ match super::NV12ToARGB(
+ src_y.as_ptr(),
+ src_stride_y as _,
+ src_uv.as_ptr(),
+ src_stride_uv as _,
+ dst.as_mut_ptr(),
+ (width * 4) as _,
+ width as _,
+ height as _,
+ ) {
+ 0 => Ok(()),
+ _ => Err(anyhow!("NV12ToARGB failed")),
+ }
+ }
+ }
+
+ pub fn hw_i420_to_bgra(
+ width: usize,
+ height: usize,
+ src_y: &[u8],
+ src_u: &[u8],
+ src_v: &[u8],
+ src_stride_y: usize,
+ src_stride_u: usize,
+ src_stride_v: usize,
+ dst: &mut Vec,
+ ) {
+ let src_y = src_y.as_ptr();
+ let src_u = src_u.as_ptr();
+ let src_v = src_v.as_ptr();
+ dst.resize(width * height * 4, 0);
+ unsafe {
+ super::I420ToARGB(
+ src_y,
+ src_stride_y as _,
+ src_u,
+ src_stride_u as _,
+ src_v,
+ src_stride_v as _,
+ dst.as_mut_ptr(),
+ (width * 4) as _,
+ width as _,
+ height as _,
+ );
+ };
+ }
+}
diff --git a/libs/scrap/src/common/dxgi.rs b/libs/scrap/src/common/dxgi.rs
index c0b4130bb..963f39de1 100644
--- a/libs/scrap/src/common/dxgi.rs
+++ b/libs/scrap/src/common/dxgi.rs
@@ -1,6 +1,12 @@
-use crate::dxgi;
-use std::io::ErrorKind::{NotFound, TimedOut, WouldBlock};
-use std::{io, ops};
+use crate::{common::TraitCapturer, dxgi};
+use std::{
+ io::{
+ self,
+ ErrorKind::{NotFound, TimedOut, WouldBlock},
+ },
+ ops,
+ time::Duration,
+};
pub struct Capturer {
inner: dxgi::Capturer,
@@ -20,14 +26,6 @@ impl Capturer {
})
}
- pub fn is_gdi(&self) -> bool {
- self.inner.is_gdi()
- }
-
- pub fn set_gdi(&mut self) -> bool {
- self.inner.set_gdi()
- }
-
pub fn cancel_gdi(&mut self) {
self.inner.cancel_gdi()
}
@@ -39,14 +37,28 @@ impl Capturer {
pub fn height(&self) -> usize {
self.height
}
+}
- pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result> {
- match self.inner.frame(timeout_ms) {
+impl TraitCapturer for Capturer {
+ fn set_use_yuv(&mut self, use_yuv: bool) {
+ self.inner.set_use_yuv(use_yuv);
+ }
+
+ fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result> {
+ match self.inner.frame(timeout.as_millis() as _) {
Ok(frame) => Ok(Frame(frame)),
Err(ref error) if error.kind() == TimedOut => Err(WouldBlock.into()),
Err(error) => Err(error),
}
}
+
+ fn is_gdi(&self) -> bool {
+ self.inner.is_gdi()
+ }
+
+ fn set_gdi(&mut self) -> bool {
+ self.inner.set_gdi()
+ }
}
pub struct Frame<'a>(&'a [u8]);
@@ -128,6 +140,7 @@ impl CapturerMag {
data: Vec::new(),
})
}
+
pub fn exclude(&mut self, cls: &str, name: &str) -> io::Result {
self.inner.exclude(cls, name)
}
@@ -135,8 +148,23 @@ impl CapturerMag {
pub fn get_rect(&self) -> ((i32, i32), usize, usize) {
self.inner.get_rect()
}
- pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result> {
+}
+
+impl TraitCapturer for CapturerMag {
+ fn set_use_yuv(&mut self, use_yuv: bool) {
+ self.inner.set_use_yuv(use_yuv)
+ }
+
+ fn frame<'a>(&'a mut self, _timeout_ms: Duration) -> io::Result> {
self.inner.frame(&mut self.data)?;
Ok(Frame(&self.data))
}
+
+ fn is_gdi(&self) -> bool {
+ false
+ }
+
+ fn set_gdi(&mut self) -> bool {
+ false
+ }
}
diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs
new file mode 100644
index 000000000..7431bc952
--- /dev/null
+++ b/libs/scrap/src/common/hwcodec.rs
@@ -0,0 +1,327 @@
+use crate::{
+ codec::{EncoderApi, EncoderCfg},
+ hw, HW_STRIDE_ALIGN,
+};
+use hbb_common::{
+ anyhow::{anyhow, Context},
+ config::HwCodecConfig,
+ lazy_static, log,
+ message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame},
+ ResultType, bytes::Bytes,
+};
+use hwcodec::{
+ decode::{DecodeContext, DecodeFrame, Decoder},
+ encode::{EncodeContext, EncodeFrame, Encoder},
+ ffmpeg::{CodecInfo, CodecInfos, DataFormat},
+ AVPixelFormat,
+ Quality::{self, *},
+ RateContorl::{self, *},
+};
+use std::sync::{Arc, Mutex};
+
+lazy_static::lazy_static! {
+ static ref HW_ENCODER_NAME: Arc>> = Default::default();
+}
+
+const CFG_KEY_ENCODER: &str = "bestHwEncoders";
+const CFG_KEY_DECODER: &str = "bestHwDecoders";
+
+const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_YUV420P;
+const DEFAULT_TIME_BASE: [i32; 2] = [1, 30];
+const DEFAULT_GOP: i32 = 60;
+const DEFAULT_HW_QUALITY: Quality = Quality_Default;
+const DEFAULT_RC: RateContorl = RC_DEFAULT;
+
+pub struct HwEncoder {
+ encoder: Encoder,
+ yuv: Vec,
+ pub format: DataFormat,
+ pub pixfmt: AVPixelFormat,
+}
+
+impl EncoderApi for HwEncoder {
+ fn new(cfg: EncoderCfg) -> ResultType
+ where
+ Self: Sized,
+ {
+ match cfg {
+ EncoderCfg::HW(config) => {
+ let ctx = EncodeContext {
+ name: config.codec_name.clone(),
+ width: config.width as _,
+ height: config.height as _,
+ pixfmt: DEFAULT_PIXFMT,
+ align: HW_STRIDE_ALIGN as _,
+ bitrate: config.bitrate * 1000,
+ timebase: DEFAULT_TIME_BASE,
+ gop: DEFAULT_GOP,
+ quality: DEFAULT_HW_QUALITY,
+ rc: DEFAULT_RC,
+ };
+ let format = match Encoder::format_from_name(config.codec_name.clone()) {
+ Ok(format) => format,
+ Err(_) => {
+ return Err(anyhow!(format!(
+ "failed to get format from name:{}",
+ config.codec_name
+ )))
+ }
+ };
+ match Encoder::new(ctx.clone()) {
+ Ok(encoder) => Ok(HwEncoder {
+ encoder,
+ yuv: vec![],
+ format,
+ pixfmt: ctx.pixfmt,
+ }),
+ Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
+ }
+ }
+ _ => Err(anyhow!("encoder type mismatch")),
+ }
+ }
+
+ fn encode_to_message(
+ &mut self,
+ frame: &[u8],
+ _ms: i64,
+ ) -> ResultType {
+ let mut msg_out = Message::new();
+ let mut vf = VideoFrame::new();
+ let mut frames = Vec::new();
+ for frame in self.encode(frame).with_context(|| "Failed to encode")? {
+ frames.push(EncodedVideoFrame {
+ data: Bytes::from(frame.data),
+ pts: frame.pts as _,
+ ..Default::default()
+ });
+ }
+ if frames.len() > 0 {
+ let frames = EncodedVideoFrames {
+ frames: frames.into(),
+ ..Default::default()
+ };
+ match self.format {
+ DataFormat::H264 => vf.set_h264s(frames),
+ DataFormat::H265 => vf.set_h265s(frames),
+ }
+ msg_out.set_video_frame(vf);
+ Ok(msg_out)
+ } else {
+ Err(anyhow!("no valid frame"))
+ }
+ }
+
+ fn use_yuv(&self) -> bool {
+ false
+ }
+
+ fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()> {
+ self.encoder.set_bitrate((bitrate * 1000) as _).ok();
+ Ok(())
+ }
+}
+
+impl HwEncoder {
+ pub fn best() -> CodecInfos {
+ get_config(CFG_KEY_ENCODER).unwrap_or(CodecInfos {
+ h264: None,
+ h265: None,
+ })
+ }
+
+ pub fn current_name() -> Arc>> {
+ HW_ENCODER_NAME.clone()
+ }
+
+ pub fn encode(&mut self, bgra: &[u8]) -> ResultType> {
+ match self.pixfmt {
+ AVPixelFormat::AV_PIX_FMT_YUV420P => hw::hw_bgra_to_i420(
+ self.encoder.ctx.width as _,
+ self.encoder.ctx.height as _,
+ &self.encoder.linesize,
+ &self.encoder.offset,
+ self.encoder.length,
+ bgra,
+ &mut self.yuv,
+ ),
+ AVPixelFormat::AV_PIX_FMT_NV12 => hw::hw_bgra_to_nv12(
+ self.encoder.ctx.width as _,
+ self.encoder.ctx.height as _,
+ &self.encoder.linesize,
+ &self.encoder.offset,
+ self.encoder.length,
+ bgra,
+ &mut self.yuv,
+ ),
+ }
+
+ match self.encoder.encode(&self.yuv) {
+ Ok(v) => {
+ let mut data = Vec::::new();
+ data.append(v);
+ Ok(data)
+ }
+ Err(_) => Ok(Vec::::new()),
+ }
+ }
+}
+
+pub struct HwDecoder {
+ decoder: Decoder,
+ pub info: CodecInfo,
+}
+
+pub struct HwDecoders {
+ pub h264: Option,
+ pub h265: Option,
+}
+
+impl HwDecoder {
+ pub fn best() -> CodecInfos {
+ get_config(CFG_KEY_DECODER).unwrap_or(CodecInfos {
+ h264: None,
+ h265: None,
+ })
+ }
+
+ pub fn new_decoders() -> HwDecoders {
+ let best = HwDecoder::best();
+ let mut h264: Option = None;
+ let mut h265: Option = None;
+ let mut fail = false;
+
+ if let Some(info) = best.h264 {
+ h264 = HwDecoder::new(info).ok();
+ if h264.is_none() {
+ fail = true;
+ }
+ }
+ if let Some(info) = best.h265 {
+ h265 = HwDecoder::new(info).ok();
+ if h265.is_none() {
+ fail = true;
+ }
+ }
+ if fail {
+ check_config_process(true);
+ }
+ HwDecoders { h264, h265 }
+ }
+
+ pub fn new(info: CodecInfo) -> ResultType {
+ let ctx = DecodeContext {
+ name: info.name.clone(),
+ device_type: info.hwdevice.clone(),
+ };
+ match Decoder::new(ctx) {
+ Ok(decoder) => Ok(HwDecoder { decoder, info }),
+ Err(_) => Err(anyhow!(format!("Failed to create decoder"))),
+ }
+ }
+ pub fn decode(&mut self, data: &[u8]) -> ResultType> {
+ match self.decoder.decode(data) {
+ Ok(v) => Ok(v.iter().map(|f| HwDecoderImage { frame: f }).collect()),
+ Err(_) => Ok(vec![]),
+ }
+ }
+}
+
+pub struct HwDecoderImage<'a> {
+ frame: &'a DecodeFrame,
+}
+
+impl HwDecoderImage<'_> {
+ pub fn bgra(&self, bgra: &mut Vec, i420: &mut Vec) -> ResultType<()> {
+ let frame = self.frame;
+ match frame.pixfmt {
+ AVPixelFormat::AV_PIX_FMT_NV12 => hw::hw_nv12_to_bgra(
+ frame.width as _,
+ frame.height as _,
+ &frame.data[0],
+ &frame.data[1],
+ frame.linesize[0] as _,
+ frame.linesize[1] as _,
+ bgra,
+ i420,
+ HW_STRIDE_ALIGN,
+ ),
+ AVPixelFormat::AV_PIX_FMT_YUV420P => {
+ hw::hw_i420_to_bgra(
+ frame.width as _,
+ frame.height as _,
+ &frame.data[0],
+ &frame.data[1],
+ &frame.data[2],
+ frame.linesize[0] as _,
+ frame.linesize[1] as _,
+ frame.linesize[2] as _,
+ bgra,
+ );
+ return Ok(());
+ }
+ }
+ }
+}
+
+fn get_config(k: &str) -> ResultType {
+ let v = HwCodecConfig::load()
+ .options
+ .get(k)
+ .unwrap_or(&"".to_owned())
+ .to_owned();
+ match CodecInfos::deserialize(&v) {
+ Ok(v) => Ok(v),
+ Err(_) => Err(anyhow!("Failed to get config:{}", k)),
+ }
+}
+
+pub fn check_config() {
+ let ctx = EncodeContext {
+ name: String::from(""),
+ width: 1920,
+ height: 1080,
+ pixfmt: DEFAULT_PIXFMT,
+ align: HW_STRIDE_ALIGN as _,
+ bitrate: 0,
+ timebase: DEFAULT_TIME_BASE,
+ gop: DEFAULT_GOP,
+ quality: DEFAULT_HW_QUALITY,
+ rc: DEFAULT_RC,
+ };
+ let encoders = CodecInfo::score(Encoder::avaliable_encoders(ctx));
+ let decoders = CodecInfo::score(Decoder::avaliable_decoders());
+
+ if let Ok(old_encoders) = get_config(CFG_KEY_ENCODER) {
+ if let Ok(old_decoders) = get_config(CFG_KEY_DECODER) {
+ if encoders == old_encoders && decoders == old_decoders {
+ return;
+ }
+ }
+ }
+
+ if let Ok(encoders) = encoders.serialize() {
+ if let Ok(decoders) = decoders.serialize() {
+ let mut config = HwCodecConfig::load();
+ config.options.insert(CFG_KEY_ENCODER.to_owned(), encoders);
+ config.options.insert(CFG_KEY_DECODER.to_owned(), decoders);
+ config.store();
+ return;
+ }
+ }
+ log::error!("Failed to serialize codec info");
+}
+
+pub fn check_config_process(force_reset: bool) {
+ if force_reset {
+ HwCodecConfig::remove();
+ }
+ if let Ok(exe) = std::env::current_exe() {
+ std::thread::spawn(move || {
+ std::process::Command::new(exe)
+ .arg("--check-hwcodec-config")
+ .status()
+ .ok()
+ });
+ };
+}
diff --git a/libs/scrap/src/common/linux.rs b/libs/scrap/src/common/linux.rs
index 50bab092c..3efcdb054 100644
--- a/libs/scrap/src/common/linux.rs
+++ b/libs/scrap/src/common/linux.rs
@@ -1,8 +1,9 @@
use crate::common::{
wayland,
x11::{self, Frame},
+ TraitCapturer,
};
-use std::io;
+use std::{io, time::Duration};
pub enum Capturer {
X11(x11::Capturer),
@@ -30,11 +31,20 @@ impl Capturer {
Capturer::WAYLAND(d) => d.height(),
}
}
+}
- pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result> {
+impl TraitCapturer for Capturer {
+ fn set_use_yuv(&mut self, use_yuv: bool) {
match self {
- Capturer::X11(d) => d.frame(timeout_ms),
- Capturer::WAYLAND(d) => d.frame(timeout_ms),
+ Capturer::X11(d) => d.set_use_yuv(use_yuv),
+ Capturer::WAYLAND(d) => d.set_use_yuv(use_yuv),
+ }
+ }
+
+ fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result> {
+ match self {
+ Capturer::X11(d) => d.frame(timeout),
+ Capturer::WAYLAND(d) => d.frame(timeout),
}
}
}
@@ -44,32 +54,26 @@ pub enum Display {
WAYLAND(wayland::Display),
}
-#[inline]
-fn is_wayland() -> bool {
- std::env::var("IS_WAYLAND").is_ok()
- || std::env::var("XDG_SESSION_TYPE") == Ok("wayland".to_owned())
-}
-
impl Display {
pub fn primary() -> io::Result {
- Ok(if is_wayland() {
- Display::WAYLAND(wayland::Display::primary()?)
- } else {
+ Ok(if super::is_x11() {
Display::X11(x11::Display::primary()?)
+ } else {
+ Display::WAYLAND(wayland::Display::primary()?)
})
}
pub fn all() -> io::Result> {
- Ok(if is_wayland() {
- wayland::Display::all()?
- .drain(..)
- .map(|x| Display::WAYLAND(x))
- .collect()
- } else {
+ Ok(if super::is_x11() {
x11::Display::all()?
.drain(..)
.map(|x| Display::X11(x))
.collect()
+ } else {
+ wayland::Display::all()?
+ .drain(..)
+ .map(|x| Display::WAYLAND(x))
+ .collect()
})
}
diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs
index 108a4ae31..8ee22ada6 100644
--- a/libs/scrap/src/common/mod.rs
+++ b/libs/scrap/src/common/mod.rs
@@ -1,4 +1,4 @@
-pub use self::codec::*;
+pub use self::vpxcodec::*;
cfg_if! {
if #[cfg(quartz)] {
@@ -11,6 +11,7 @@ cfg_if! {
mod wayland;
mod x11;
pub use self::linux::*;
+ pub use self::x11::Frame;
} else {
mod x11;
pub use self::x11::*;
@@ -29,8 +30,12 @@ cfg_if! {
pub mod codec;
mod convert;
+#[cfg(feature = "hwcodec")]
+pub mod hwcodec;
+pub mod vpxcodec;
pub use self::convert::*;
pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller
+pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer
mod vpx;
@@ -44,3 +49,19 @@ pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()
old.copy_from_slice(b);
Ok(())
}
+
+pub trait TraitCapturer {
+ fn set_use_yuv(&mut self, use_yuv: bool);
+ fn frame<'a>(&'a mut self, timeout: std::time::Duration) -> std::io::Result>;
+
+ #[cfg(windows)]
+ fn is_gdi(&self) -> bool;
+ #[cfg(windows)]
+ fn set_gdi(&mut self) -> bool;
+}
+
+#[cfg(x11)]
+#[inline]
+pub fn is_x11() -> bool {
+ "x11" == hbb_common::platform::linux::get_display_server()
+}
diff --git a/libs/scrap/src/common/quartz.rs b/libs/scrap/src/common/quartz.rs
index b35b56b61..6e29c2441 100644
--- a/libs/scrap/src/common/quartz.rs
+++ b/libs/scrap/src/common/quartz.rs
@@ -50,8 +50,14 @@ impl Capturer {
pub fn height(&self) -> usize {
self.inner.height()
}
+}
- pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result> {
+impl crate::TraitCapturer for Capturer {
+ fn set_use_yuv(&mut self, use_yuv: bool) {
+ self.use_yuv = use_yuv;
+ }
+
+ fn frame<'a>(&'a mut self, _timeout_ms: std::time::Duration) -> io::Result> {
match self.frame.try_lock() {
Ok(mut handle) => {
let mut frame = None;
diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs
new file mode 100644
index 000000000..0fda53fa3
--- /dev/null
+++ b/libs/scrap/src/common/vpxcodec.rs
@@ -0,0 +1,600 @@
+// https://github.com/astraw/vpx-encode
+// https://github.com/astraw/env-libvpx-sys
+// https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs
+
+use hbb_common::anyhow::{anyhow, Context};
+use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame};
+use hbb_common::ResultType;
+
+use crate::codec::EncoderApi;
+use crate::STRIDE_ALIGN;
+
+use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *};
+use std::os::raw::{c_int, c_uint};
+use std::{ptr, slice};
+use hbb_common::bytes::Bytes;
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub enum VpxVideoCodecId {
+ VP8,
+ VP9,
+}
+
+impl Default for VpxVideoCodecId {
+ fn default() -> VpxVideoCodecId {
+ VpxVideoCodecId::VP9
+ }
+}
+
+pub struct VpxEncoder {
+ ctx: vpx_codec_ctx_t,
+ width: usize,
+ height: usize,
+}
+
+pub struct VpxDecoder {
+ ctx: vpx_codec_ctx_t,
+}
+
+#[derive(Debug)]
+pub enum Error {
+ FailedCall(String),
+ BadPtr(String),
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl std::error::Error for Error {}
+
+pub type Result = std::result::Result;
+
+macro_rules! call_vpx {
+ ($x:expr) => {{
+ let result = unsafe { $x }; // original expression
+ let result_int = unsafe { std::mem::transmute::<_, i32>(result) };
+ if result_int != 0 {
+ return Err(Error::FailedCall(format!(
+ "errcode={} {}:{}:{}:{}",
+ result_int,
+ module_path!(),
+ file!(),
+ line!(),
+ column!()
+ ))
+ .into());
+ }
+ result
+ }};
+}
+
+macro_rules! call_vpx_ptr {
+ ($x:expr) => {{
+ let result = unsafe { $x }; // original expression
+ let result_int = unsafe { std::mem::transmute::<_, isize>(result) };
+ if result_int == 0 {
+ return Err(Error::BadPtr(format!(
+ "errcode={} {}:{}:{}:{}",
+ result_int,
+ module_path!(),
+ file!(),
+ line!(),
+ column!()
+ ))
+ .into());
+ }
+ result
+ }};
+}
+
+impl EncoderApi for VpxEncoder {
+ fn new(cfg: crate::codec::EncoderCfg) -> ResultType
+ where
+ Self: Sized,
+ {
+ match cfg {
+ crate::codec::EncoderCfg::VPX(config) => {
+ let i;
+ if cfg!(feature = "VP8") {
+ i = match config.codec {
+ VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()),
+ VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()),
+ };
+ } else {
+ i = call_vpx_ptr!(vpx_codec_vp9_cx());
+ }
+ let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
+ call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0));
+
+ // https://www.webmproject.org/docs/encoder-parameters/
+ // default: c.rc_min_quantizer = 0, c.rc_max_quantizer = 63
+ // try rc_resize_allowed later
+
+ c.g_w = config.width;
+ c.g_h = config.height;
+ c.g_timebase.num = config.timebase[0];
+ c.g_timebase.den = config.timebase[1];
+ c.rc_target_bitrate = config.bitrate;
+ c.rc_undershoot_pct = 95;
+ c.rc_dropframe_thresh = 25;
+ c.g_threads = if config.num_threads == 0 {
+ num_cpus::get() as _
+ } else {
+ config.num_threads
+ };
+ c.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
+ // https://developers.google.com/media/vp9/bitrate-modes/
+ // Constant Bitrate mode (CBR) is recommended for live streaming with VP9.
+ c.rc_end_usage = vpx_rc_mode::VPX_CBR;
+ // c.kf_min_dist = 0;
+ // c.kf_max_dist = 999999;
+ c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot
+
+ /*
+ VPX encoder支持two-pass encode,这是为了rate control的。
+ 对于两遍编码,就是需要整个编码过程做两次,第一次会得到一些新的控制参数来进行第二遍的编码,
+ 这样可以在相同的bitrate下得到最好的PSNR
+ */
+
+ let mut ctx = Default::default();
+ call_vpx!(vpx_codec_enc_init_ver(
+ &mut ctx,
+ i,
+ &c,
+ 0,
+ VPX_ENCODER_ABI_VERSION as _
+ ));
+
+ if config.codec == VpxVideoCodecId::VP9 {
+ // set encoder internal speed settings
+ // in ffmpeg, it is --speed option
+ /*
+ set to 0 or a positive value 1-16, the codec will try to adapt its
+ complexity depending on the time it spends encoding. Increasing this
+ number will make the speed go up and the quality go down.
+ Negative values mean strict enforcement of this
+ while positive values are adaptive
+ */
+ /* https://developers.google.com/media/vp9/live-encoding
+ Speed 5 to 8 should be used for live / real-time encoding.
+ Lower numbers (5 or 6) are higher quality but require more CPU power.
+ Higher numbers (7 or 8) will be lower quality but more manageable for lower latency
+ use cases and also for lower CPU power devices such as mobile.
+ */
+ call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, 7,));
+ // set row level multi-threading
+ /*
+ as some people in comments and below have already commented,
+ more recent versions of libvpx support -row-mt 1 to enable tile row
+ multi-threading. This can increase the number of tiles by up to 4x in VP9
+ (since the max number of tile rows is 4, regardless of video height).
+ To enable this, use -tile-rows N where N is the number of tile rows in
+ log2 units (so -tile-rows 1 means 2 tile rows and -tile-rows 2 means 4 tile
+ rows). The total number of active threads will then be equal to
+ $tile_rows * $tile_columns
+ */
+ call_vpx!(vpx_codec_control_(
+ &mut ctx,
+ VP9E_SET_ROW_MT as _,
+ 1 as c_int
+ ));
+
+ call_vpx!(vpx_codec_control_(
+ &mut ctx,
+ VP9E_SET_TILE_COLUMNS as _,
+ 4 as c_int
+ ));
+ }
+
+ Ok(Self {
+ ctx,
+ width: config.width as _,
+ height: config.height as _,
+ })
+ }
+ _ => Err(anyhow!("encoder type mismatch")),
+ }
+ }
+
+ fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType {
+ let mut frames = Vec::new();
+ for ref frame in self
+ .encode(ms, frame, STRIDE_ALIGN)
+ .with_context(|| "Failed to encode")?
+ {
+ frames.push(VpxEncoder::create_frame(frame));
+ }
+ for ref frame in self.flush().with_context(|| "Failed to flush")? {
+ frames.push(VpxEncoder::create_frame(frame));
+ }
+
+ // to-do: flush periodically, e.g. 1 second
+ if frames.len() > 0 {
+ Ok(VpxEncoder::create_msg(frames))
+ } else {
+ Err(anyhow!("no valid frame"))
+ }
+ }
+
+ fn use_yuv(&self) -> bool {
+ true
+ }
+
+ fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()> {
+ let mut new_enc_cfg = unsafe { *self.ctx.config.enc.to_owned() };
+ new_enc_cfg.rc_target_bitrate = bitrate;
+ call_vpx!(vpx_codec_enc_config_set(&mut self.ctx, &new_enc_cfg));
+ return Ok(());
+ }
+}
+
+impl VpxEncoder {
+ pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result {
+ assert!(2 * data.len() >= 3 * self.width * self.height);
+
+ let mut image = Default::default();
+ call_vpx_ptr!(vpx_img_wrap(
+ &mut image,
+ vpx_img_fmt::VPX_IMG_FMT_I420,
+ self.width as _,
+ self.height as _,
+ stride_align as _,
+ data.as_ptr() as _,
+ ));
+
+ call_vpx!(vpx_codec_encode(
+ &mut self.ctx,
+ &image,
+ pts as _,
+ 1, // Duration
+ 0, // Flags
+ VPX_DL_REALTIME as _,
+ ));
+
+ Ok(EncodeFrames {
+ ctx: &mut self.ctx,
+ iter: ptr::null(),
+ })
+ }
+
+ /// Notify the encoder to return any pending packets
+ pub fn flush(&mut self) -> Result {
+ call_vpx!(vpx_codec_encode(
+ &mut self.ctx,
+ ptr::null(),
+ -1, // PTS
+ 1, // Duration
+ 0, // Flags
+ VPX_DL_REALTIME as _,
+ ));
+
+ Ok(EncodeFrames {
+ ctx: &mut self.ctx,
+ iter: ptr::null(),
+ })
+ }
+
+ #[inline]
+ fn create_msg(vp9s: Vec) -> Message {
+ let mut msg_out = Message::new();
+ let mut vf = VideoFrame::new();
+ vf.set_vp9s(EncodedVideoFrames {
+ frames: vp9s.into(),
+ ..Default::default()
+ });
+ msg_out.set_video_frame(vf);
+ msg_out
+ }
+
+ #[inline]
+ fn create_frame(frame: &EncodeFrame) -> EncodedVideoFrame {
+ EncodedVideoFrame {
+ data: Bytes::from(frame.data.to_vec()),
+ key: frame.key,
+ pts: frame.pts,
+ ..Default::default()
+ }
+ }
+}
+
+impl Drop for VpxEncoder {
+ fn drop(&mut self) {
+ unsafe {
+ let result = vpx_codec_destroy(&mut self.ctx);
+ if result != VPX_CODEC_OK {
+ panic!("failed to destroy vpx codec");
+ }
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct EncodeFrame<'a> {
+ /// Compressed data.
+ pub data: &'a [u8],
+ /// Whether the frame is a keyframe.
+ pub key: bool,
+ /// Presentation timestamp (in timebase units).
+ pub pts: i64,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct VpxEncoderConfig {
+ /// The width (in pixels).
+ pub width: c_uint,
+ /// The height (in pixels).
+ pub height: c_uint,
+ /// The timebase numerator and denominator (in seconds).
+ pub timebase: [c_int; 2],
+ /// The target bitrate (in kilobits per second).
+ pub bitrate: c_uint,
+ /// The codec
+ pub codec: VpxVideoCodecId,
+ pub num_threads: u32,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct VpxDecoderConfig {
+ pub codec: VpxVideoCodecId,
+ pub num_threads: u32,
+}
+
+pub struct EncodeFrames<'a> {
+ ctx: &'a mut vpx_codec_ctx_t,
+ iter: vpx_codec_iter_t,
+}
+
+impl<'a> Iterator for EncodeFrames<'a> {
+ type Item = EncodeFrame<'a>;
+ fn next(&mut self) -> Option {
+ loop {
+ unsafe {
+ let pkt = vpx_codec_get_cx_data(self.ctx, &mut self.iter);
+ if pkt.is_null() {
+ return None;
+ } else if (*pkt).kind == vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT {
+ let f = &(*pkt).data.frame;
+ return Some(Self::Item {
+ data: slice::from_raw_parts(f.buf as _, f.sz as _),
+ key: (f.flags & VPX_FRAME_IS_KEY) != 0,
+ pts: f.pts,
+ });
+ } else {
+ // Ignore the packet.
+ }
+ }
+ }
+ }
+}
+
+impl VpxDecoder {
+ /// Create a new decoder
+ ///
+ /// # Errors
+ ///
+ /// The function may fail if the underlying libvpx does not provide
+ /// the VP9 decoder.
+ pub fn new(config: VpxDecoderConfig) -> Result {
+ // This is sound because `vpx_codec_ctx` is a repr(C) struct without any field that can
+ // cause UB if uninitialized.
+ let i;
+ if cfg!(feature = "VP8") {
+ i = match config.codec {
+ VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()),
+ VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()),
+ };
+ } else {
+ i = call_vpx_ptr!(vpx_codec_vp9_dx());
+ }
+ let mut ctx = Default::default();
+ let cfg = vpx_codec_dec_cfg_t {
+ threads: if config.num_threads == 0 {
+ num_cpus::get() as _
+ } else {
+ config.num_threads
+ },
+ w: 0,
+ h: 0,
+ };
+ /*
+ unsafe {
+ println!("{}", vpx_codec_get_caps(i));
+ }
+ */
+ call_vpx!(vpx_codec_dec_init_ver(
+ &mut ctx,
+ i,
+ &cfg,
+ 0,
+ VPX_DECODER_ABI_VERSION as _,
+ ));
+ Ok(Self { ctx })
+ }
+
+ pub fn decode2rgb(&mut self, data: &[u8], rgba: bool) -> Result> {
+ let mut img = Image::new();
+ for frame in self.decode(data)? {
+ drop(img);
+ img = frame;
+ }
+ for frame in self.flush()? {
+ drop(img);
+ img = frame;
+ }
+ if img.is_null() {
+ Ok(Vec::new())
+ } else {
+ let mut out = Default::default();
+ img.rgb(1, rgba, &mut out);
+ Ok(out)
+ }
+ }
+
+ /// Feed some compressed data to the encoder
+ ///
+ /// The `data` slice is sent to the decoder
+ ///
+ /// It matches a call to `vpx_codec_decode`.
+ pub fn decode(&mut self, data: &[u8]) -> Result {
+ call_vpx!(vpx_codec_decode(
+ &mut self.ctx,
+ data.as_ptr(),
+ data.len() as _,
+ ptr::null_mut(),
+ 0,
+ ));
+
+ Ok(DecodeFrames {
+ ctx: &mut self.ctx,
+ iter: ptr::null(),
+ })
+ }
+
+ /// Notify the decoder to return any pending frame
+ pub fn flush(&mut self) -> Result {
+ call_vpx!(vpx_codec_decode(
+ &mut self.ctx,
+ ptr::null(),
+ 0,
+ ptr::null_mut(),
+ 0
+ ));
+ Ok(DecodeFrames {
+ ctx: &mut self.ctx,
+ iter: ptr::null(),
+ })
+ }
+}
+
+impl Drop for VpxDecoder {
+ fn drop(&mut self) {
+ unsafe {
+ let result = vpx_codec_destroy(&mut self.ctx);
+ if result != VPX_CODEC_OK {
+ panic!("failed to destroy vpx codec");
+ }
+ }
+ }
+}
+
+pub struct DecodeFrames<'a> {
+ ctx: &'a mut vpx_codec_ctx_t,
+ iter: vpx_codec_iter_t,
+}
+
+impl<'a> Iterator for DecodeFrames<'a> {
+ type Item = Image;
+ fn next(&mut self) -> Option {
+ let img = unsafe { vpx_codec_get_frame(self.ctx, &mut self.iter) };
+ if img.is_null() {
+ return None;
+ } else {
+ return Some(Image(img));
+ }
+ }
+}
+
+// https://chromium.googlesource.com/webm/libvpx/+/bali/vpx/src/vpx_image.c
+pub struct Image(*mut vpx_image_t);
+impl Image {
+ #[inline]
+ pub fn new() -> Self {
+ Self(std::ptr::null_mut())
+ }
+
+ #[inline]
+ pub fn is_null(&self) -> bool {
+ self.0.is_null()
+ }
+
+ #[inline]
+ pub fn width(&self) -> usize {
+ self.inner().d_w as _
+ }
+
+ #[inline]
+ pub fn height(&self) -> usize {
+ self.inner().d_h as _
+ }
+
+ #[inline]
+ pub fn format(&self) -> vpx_img_fmt_t {
+ // VPX_IMG_FMT_I420
+ self.inner().fmt
+ }
+
+ #[inline]
+ pub fn inner(&self) -> &vpx_image_t {
+ unsafe { &*self.0 }
+ }
+
+ #[inline]
+ pub fn stride(&self, iplane: usize) -> i32 {
+ self.inner().stride[iplane]
+ }
+
+ pub fn rgb(&self, stride_align: usize, rgba: bool, dst: &mut Vec) {
+ let h = self.height();
+ let mut w = self.width();
+ let bps = if rgba { 4 } else { 3 };
+ w = (w + stride_align - 1) & !(stride_align - 1);
+ dst.resize(h * w * bps, 0);
+ let img = self.inner();
+ unsafe {
+ if rgba {
+ super::I420ToARGB(
+ img.planes[0],
+ img.stride[0],
+ img.planes[1],
+ img.stride[1],
+ img.planes[2],
+ img.stride[2],
+ dst.as_mut_ptr(),
+ (w * bps) as _,
+ self.width() as _,
+ self.height() as _,
+ );
+ } else {
+ super::I420ToRAW(
+ img.planes[0],
+ img.stride[0],
+ img.planes[1],
+ img.stride[1],
+ img.planes[2],
+ img.stride[2],
+ dst.as_mut_ptr(),
+ (w * bps) as _,
+ self.width() as _,
+ self.height() as _,
+ );
+ }
+ }
+ }
+
+ #[inline]
+ pub fn data(&self) -> (&[u8], &[u8], &[u8]) {
+ unsafe {
+ let img = self.inner();
+ let h = (img.d_h as usize + 1) & !1;
+ let n = img.stride[0] as usize * h;
+ let y = slice::from_raw_parts(img.planes[0], n);
+ let n = img.stride[1] as usize * (h >> 1);
+ let u = slice::from_raw_parts(img.planes[1], n);
+ let v = slice::from_raw_parts(img.planes[2], n);
+ (y, u, v)
+ }
+ }
+}
+
+impl Drop for Image {
+ fn drop(&mut self) {
+ if !self.0.is_null() {
+ unsafe { vpx_img_free(self.0) };
+ }
+ }
+}
+
+unsafe impl Send for vpx_codec_ctx_t {}
diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs
index ff6bf8022..e33cbe745 100644
--- a/libs/scrap/src/common/wayland.rs
+++ b/libs/scrap/src/common/wayland.rs
@@ -1,6 +1,6 @@
-use crate::common::x11::Frame;
+use crate::common::{x11::Frame, TraitCapturer};
use crate::wayland::{capturable::*, *};
-use std::io;
+use std::{io, time::Duration};
pub struct Capturer(Display, Box, bool, Vec);
@@ -21,9 +21,15 @@ impl Capturer {
pub fn height(&self) -> usize {
self.0.height()
}
+}
- pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result> {
- match self.1.capture(timeout_ms as _).map_err(map_err)? {
+impl TraitCapturer for Capturer {
+ fn set_use_yuv(&mut self, use_yuv: bool) {
+ self.2 = use_yuv;
+ }
+
+ fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result> {
+ match self.1.capture(timeout.as_millis() as _).map_err(map_err)? {
PixelProvider::BGR0(w, h, x) => Ok(Frame(if self.2 {
crate::common::bgra_to_i420(w as _, h as _, &x, &mut self.3);
&self.3[..]
diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs
index f8217e3b7..791514deb 100644
--- a/libs/scrap/src/common/x11.rs
+++ b/libs/scrap/src/common/x11.rs
@@ -1,5 +1,5 @@
-use crate::x11;
-use std::{io, ops};
+use crate::{x11, common::TraitCapturer};
+use std::{io, ops, time::Duration};
pub struct Capturer(x11::Capturer);
@@ -15,8 +15,14 @@ impl Capturer {
pub fn height(&self) -> usize {
self.0.display().rect().h as usize
}
+}
- pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result> {
+impl TraitCapturer for Capturer {
+ fn set_use_yuv(&mut self, use_yuv: bool) {
+ self.0.set_use_yuv(use_yuv);
+ }
+
+ fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result> {
Ok(Frame(self.0.frame()?))
}
}
diff --git a/libs/scrap/src/dxgi/mag.rs b/libs/scrap/src/dxgi/mag.rs
index 0d63088b7..78f14194c 100644
--- a/libs/scrap/src/dxgi/mag.rs
+++ b/libs/scrap/src/dxgi/mag.rs
@@ -282,7 +282,11 @@ impl CapturerMag {
let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
let w = GetSystemMetrics(SM_CXVIRTUALSCREEN);
let h = GetSystemMetrics(SM_CYVIRTUALSCREEN);
- if !(origin.0 == x as _ && origin.1 == y as _ && width == w as _ && height == h as _) {
+ if !(origin.0 == x as i32
+ && origin.1 == y as i32
+ && width == w as usize
+ && height == h as usize)
+ {
return Err(Error::new(
ErrorKind::Other,
format!(
@@ -442,6 +446,10 @@ impl CapturerMag {
Ok(s)
}
+ pub(crate) fn set_use_yuv(&mut self, use_yuv: bool) {
+ self.use_yuv = use_yuv;
+ }
+
pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result {
let name_c = CString::new(name).unwrap();
unsafe {
@@ -510,10 +518,10 @@ impl CapturerMag {
let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
let w = GetSystemMetrics(SM_CXVIRTUALSCREEN);
let h = GetSystemMetrics(SM_CYVIRTUALSCREEN);
- if !(self.rect.left == x as _
- && self.rect.top == y as _
- && self.rect.right == (x + w) as _
- && self.rect.bottom == (y + h) as _)
+ if !(self.rect.left == x as i32
+ && self.rect.top == y as i32
+ && self.rect.right == (x + w) as i32
+ && self.rect.bottom == (y + h) as i32)
{
return Err(Error::new(
ErrorKind::Other,
diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs
index 46692535d..6b60b256d 100644
--- a/libs/scrap/src/dxgi/mod.rs
+++ b/libs/scrap/src/dxgi/mod.rs
@@ -156,6 +156,10 @@ impl Capturer {
})
}
+ pub fn set_use_yuv(&mut self, use_yuv: bool) {
+ self.use_yuv = use_yuv;
+ }
+
pub fn is_gdi(&self) -> bool {
self.gdi_capturer.is_some()
}
diff --git a/libs/scrap/src/x11/capturer.rs b/libs/scrap/src/x11/capturer.rs
index 890b9db63..ed424c35a 100644
--- a/libs/scrap/src/x11/capturer.rs
+++ b/libs/scrap/src/x11/capturer.rs
@@ -74,6 +74,10 @@ impl Capturer {
Ok(c)
}
+ pub fn set_use_yuv(&mut self, use_yuv: bool) {
+ self.use_yuv = use_yuv;
+ }
+
pub fn display(&self) -> &Display {
&self.display
}
diff --git a/pynput_service.py b/pynput_service.py
index 90b8741ce..c51e9a524 100644
--- a/pynput_service.py
+++ b/pynput_service.py
@@ -1,31 +1,177 @@
from pynput.keyboard import Key, Controller
from pynput.keyboard._xorg import KeyCode
from pynput._util.xorg import display_manager
+import Xlib
+from pynput._util.xorg import *
+import Xlib
import os
import sys
import socket
-from Xlib.ext.xtest import fake_input
-from Xlib import X
-import Xlib
KeyCode._from_symbol("\0") # test
+DEAD_KEYS = {
+ '`': 65104,
+ '´': 65105,
+ '^': 65106,
+ '~': 65107,
+ '¯': 65108,
+ '˘': 65109,
+ '˙': 65110,
+ '¨': 65111,
+ '˚': 65112,
+ '˝': 65113,
+ 'ˇ': 65114,
+ '¸': 65115,
+ '˛': 65116,
+ '℩': 65117, # ?
+ '゛': 65118, # ?
+ '゚ ': 65119,
+ 'ٜ': 65120,
+ '↪': 65121,
+ ' ̛': 65122,
+}
+
+
+
+def my_keyboard_mapping(display):
+ """Generates a mapping from *keysyms* to *key codes* and required
+ modifier shift states.
+
+ :param Xlib.display.Display display: The display for which to retrieve the
+ keyboard mapping.
+
+ :return: the keyboard mapping
+ """
+ mapping = {}
+
+ shift_mask = 1 << 0
+ group_mask = alt_gr_mask(display)
+
+ # Iterate over all keysym lists in the keyboard mapping
+ min_keycode = display.display.info.min_keycode
+ keycode_count = display.display.info.max_keycode - min_keycode + 1
+ for index, keysyms in enumerate(display.get_keyboard_mapping(
+ min_keycode, keycode_count)):
+ key_code = index + min_keycode
+
+ # Normalise the keysym list to yield a tuple containing the two groups
+ normalized = keysym_normalize(keysyms)
+ if not normalized:
+ continue
+
+ # Iterate over the groups to extract the shift and modifier state
+ for groups, group in zip(normalized, (False, True)):
+ for keysym, shift in zip(groups, (False, True)):
+
+ if not keysym:
+ continue
+ shift_state = 0 \
+ | (shift_mask if shift else 0) \
+ | (group_mask if group else 0)
+
+ # !!!: Save all keycode combinations of keysym
+ if keysym in mapping:
+ mapping[keysym].append((key_code, shift_state))
+ else:
+ mapping[keysym] = [(key_code, shift_state)]
+ return mapping
+
class MyController(Controller):
- def _handle(self, key, is_press):
- """Resolves a key identifier and sends a keyboard event.
- :param event: The *X* keyboard event.
- :param int keysym: The keysym to handle.
+ def _update_keyboard_mapping(self):
+ """Updates the keyboard mapping.
"""
- keysym = self._keysym(key)
- keycode = self._display.keysym_to_keycode(keysym)
+ with display_manager(self._display) as dm:
+ self._keyboard_mapping = my_keyboard_mapping(dm)
+ def send_event(self, event, keycode, shift_state):
+ with display_manager(self._display) as dm, self.modifiers as modifiers:
+ # Under certain cimcumstances, such as when running under Xephyr,
+ # the value returned by dm.get_input_focus is an int
+ window = dm.get_input_focus().focus
+ send_event = getattr(
+ window,
+ 'send_event',
+ lambda event: dm.send_event(window, event))
+ send_event(event(
+ detail=keycode,
+ state=shift_state | self._shift_mask(modifiers),
+ time=0,
+ root=dm.screen().root,
+ window=window,
+ same_screen=0,
+ child=Xlib.X.NONE,
+ root_x=0, root_y=0, event_x=0, event_y=0))
+
+ def fake_input(self, keycode, is_press):
with display_manager(self._display) as dm:
Xlib.ext.xtest.fake_input(
dm,
Xlib.X.KeyPress if is_press else Xlib.X.KeyRelease,
keycode)
+ def _handle(self, key, is_press):
+ """Resolves a key identifier and sends a keyboard event.
+ :param event: The *X* keyboard event.
+ :param int keysym: The keysym to handle.
+ """
+ event = Xlib.display.event.KeyPress if is_press \
+ else Xlib.display.event.KeyRelease
+ keysym = self._keysym(key)
+
+ if key.vk is not None:
+ keycode = self._display.keysym_to_keycode(key.vk)
+ self.fake_input(keycode, is_press)
+ # Otherwise use XSendEvent; we need to use this in the general case to
+ # work around problems with keyboard layouts
+ self._emit('_on_fake_event', key, is_press)
+ return
+
+ # Make sure to verify that the key was resolved
+ if keysym is None:
+ raise self.InvalidKeyException(key)
+
+ # There may be multiple keycodes for keysym in keyboard_mapping
+ keycode_flag = len(self.keyboard_mapping[keysym]) == 1
+ if keycode_flag:
+ keycode, shift_state = self.keyboard_mapping[keysym][0]
+ else:
+ keycode, shift_state = self._display.keysym_to_keycode(keysym), 0
+
+ keycode_set = set(map(lambda x: x[0], self.keyboard_mapping[keysym]))
+ # The keycode of the dead key is inconsistent, The keysym has multiple combinations of a keycode.
+ if keycode != self._display.keysym_to_keycode(keysym) \
+ or (keycode_flag == False and keycode == list(keycode_set)[0] and len(keycode_set) == 1):
+ deakkey_chr = str(key).replace("'", '')
+ keysym = DEAD_KEYS[deakkey_chr]
+ keycode, shift_state = self.keyboard_mapping[keysym][0]
+
+ # If the key has a virtual key code, use that immediately with
+ # fake_input; fake input,being an X server extension, has access to
+ # more internal state that we do
+
+ try:
+ with self.modifiers as modifiers:
+ alt_gr = Key.alt_gr in modifiers
+ # !!!: Send_event can't support lock screen, this condition cann't be modified
+ if alt_gr:
+ self.send_event(
+ event, keycode, shift_state)
+ else:
+ self.fake_input(keycode, is_press)
+ except KeyError:
+ with self._borrow_lock:
+ keycode, index, count = self._borrows[keysym]
+ self._send_key(
+ event,
+ keycode,
+ index_to_shift(self._display, index))
+ count += 1 if is_press else -1
+ self._borrows[keysym] = (keycode, index, count)
+
+ # Notify any running listeners
+ self._emit('_on_fake_event', key, is_press)
keyboard = MyController()
@@ -82,7 +228,7 @@ def loop():
else:
keyboard.release(name)
except Exception as e:
- print(e)
+ print('[x] error key',e)
loop()
diff --git a/rpm-suse.spec b/rpm-suse.spec
index 8863f3da2..16c81ae90 100644
--- a/rpm-suse.spec
+++ b/rpm-suse.spec
@@ -47,7 +47,7 @@ case "$1" in
;;
2)
# for upgrade
- service rustdesk stop || true
+ systemctl stop rustdesk || true
;;
esac
@@ -61,10 +61,26 @@ systemctl start rustdesk
update-desktop-database
%preun
-systemctl stop rustdesk || true
-systemctl disable rustdesk || true
-rm /etc/systemd/system/rustdesk.service || true
+case "$1" in
+ 0)
+ # for uninstall
+ systemctl stop rustdesk || true
+ systemctl disable rustdesk || true
+ rm /etc/systemd/system/rustdesk.service || true
+ ;;
+ 1)
+ # for upgrade
+ ;;
+esac
%postun
-rm /usr/share/applications/rustdesk.desktop || true
-update-desktop-database
+case "$1" in
+ 0)
+ # for uninstall
+ rm /usr/share/applications/rustdesk.desktop || true
+ update-desktop-database
+ ;;
+ 1)
+ # for upgrade
+ ;;
+esac
diff --git a/rpm.spec b/rpm.spec
index ff32d7b97..707f0381a 100644
--- a/rpm.spec
+++ b/rpm.spec
@@ -48,7 +48,7 @@ case "$1" in
;;
2)
# for upgrade
- service rustdesk stop || true
+ systemctl stop rustdesk || true
;;
esac
@@ -62,10 +62,26 @@ systemctl start rustdesk
update-desktop-database
%preun
-systemctl stop rustdesk || true
-systemctl disable rustdesk || true
-rm /etc/systemd/system/rustdesk.service || true
+case "$1" in
+ 0)
+ # for uninstall
+ systemctl stop rustdesk || true
+ systemctl disable rustdesk || true
+ rm /etc/systemd/system/rustdesk.service || true
+ ;;
+ 1)
+ # for upgrade
+ ;;
+esac
%postun
-rm /usr/share/applications/rustdesk.desktop || true
-update-desktop-database
+case "$1" in
+ 0)
+ # for uninstall
+ rm /usr/share/applications/rustdesk.desktop || true
+ update-desktop-database
+ ;;
+ 1)
+ # for upgrade
+ ;;
+esac
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 000000000..05dfa3270
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "1.62.0"
diff --git a/rustdesk.service b/rustdesk.service
index af4a3c411..e703b056f 100644
--- a/rustdesk.service
+++ b/rustdesk.service
@@ -6,7 +6,7 @@ After=systemd-user-sessions.service
[Service]
Type=simple
ExecStart=/usr/bin/rustdesk --service
-PIDFile=/var/run/rustdesk.pid
+PIDFile=/run/rustdesk.pid
KillMode=mixed
TimeoutStopSec=30
User=root
diff --git a/rustdesk.service.user b/rustdesk.service.user
new file mode 100644
index 000000000..f6c7454c9
--- /dev/null
+++ b/rustdesk.service.user
@@ -0,0 +1,15 @@
+[Unit]
+Description=RustDesk user service (--server)
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/rustdesk --server
+PIDFile=/run/rustdesk.user.pid
+KillMode=mixed
+TimeoutStopSec=30
+LimitNOFILE=100000
+Restart=on-failure
+RestartSec=3
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/client.rs b/src/client.rs
index 8457fcd38..478d81ce8 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -24,6 +24,7 @@ use hbb_common::{
log,
message_proto::{option_message::BoolOption, *},
protobuf::Message as _,
+ rand,
rendezvous_proto::*,
socket_client,
sodiumoxide::crypto::{box_, secretbox, sign},
@@ -32,7 +33,12 @@ use hbb_common::{
AddrMangle, ResultType, Stream,
};
pub use helper::LatencyController;
-use scrap::{Decoder, Image, VideoCodecId};
+pub use helper::*;
+use scrap::Image;
+use scrap::{
+ codec::{Decoder, DecoderCfg},
+ VpxDecoderConfig, VpxVideoCodecId,
+};
pub use super::lang::*;
@@ -149,11 +155,25 @@ impl Client {
true,
));
}
- let rendezvous_server = crate::get_rendezvous_server(1_000).await;
- log::info!("rendezvous server: {}", rendezvous_server);
-
+ let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await;
let mut socket =
- socket_client::connect_tcp(&*rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT).await?;
+ socket_client::connect_tcp(&*rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT).await;
+ debug_assert!(!servers.contains(&rendezvous_server));
+ if socket.is_err() && !servers.is_empty() {
+ log::info!("try the other servers: {:?}", servers);
+ for server in servers {
+ socket = socket_client::connect_tcp(&*server, any_addr, RENDEZVOUS_TIMEOUT).await;
+ if socket.is_ok() {
+ rendezvous_server = server;
+ break;
+ }
+ }
+ crate::refresh_rendezvous_server();
+ } else if !contained {
+ crate::refresh_rendezvous_server();
+ }
+ log::info!("rendezvous server: {}", rendezvous_server);
+ let mut socket = socket?;
let my_addr = socket.local_addr();
let mut signed_id_pk = Vec::new();
let mut relay_server = "".to_owned();
@@ -166,7 +186,7 @@ impl Client {
for i in 1..=3 {
log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer);
let mut msg_out = RendezvousMessage::new();
- use hbb_common::protobuf::ProtobufEnum;
+ use hbb_common::protobuf::Enum;
let nat_type = NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT);
msg_out.set_punch_hole_request(PunchHoleRequest {
id: peer.to_owned(),
@@ -180,7 +200,7 @@ impl Client {
if let Some(Ok(bytes)) = socket.next_timeout(i * 6000).await {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
match msg_in.union {
- Some(rendezvous_message::Union::punch_hole_response(ph)) => {
+ Some(rendezvous_message::Union::PunchHoleResponse(ph)) => {
if ph.socket_addr.is_empty() {
if !ph.other_failure.is_empty() {
bail!(ph.other_failure);
@@ -200,22 +220,22 @@ impl Client {
}
}
} else {
- peer_nat_type = ph.get_nat_type();
- is_local = ph.get_is_local();
- signed_id_pk = ph.pk;
+ peer_nat_type = ph.nat_type();
+ is_local = ph.is_local();
+ signed_id_pk = ph.pk.into();
relay_server = ph.relay_server;
peer_addr = AddrMangle::decode(&ph.socket_addr);
log::info!("Hole Punched {} = {}", peer, peer_addr);
break;
}
}
- Some(rendezvous_message::Union::relay_response(rr)) => {
+ Some(rendezvous_message::Union::RelayResponse(rr)) => {
log::info!(
"relay requested from peer, time used: {:?}, relay_server: {}",
start.elapsed(),
rr.relay_server
);
- signed_id_pk = rr.get_pk().into();
+ signed_id_pk = rr.pk().into();
let mut conn =
Self::create_relay(peer, rr.uuid, rr.relay_server, key, conn_type)
.await?;
@@ -359,7 +379,7 @@ impl Client {
conn: &mut Stream,
) -> ResultType<()> {
let rs_pk = get_rs_pk(if key.is_empty() {
- "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="
+ hbb_common::config::RS_PUB_KEY
} else {
key
});
@@ -386,7 +406,7 @@ impl Client {
Some(res) => {
let bytes = res?;
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
- if let Some(message::Union::signed_id(si)) = msg_in.union {
+ if let Some(message::Union::SignedId(si)) = msg_in.union {
if let Ok((id, their_pk_b)) = decode_id_pk(&si.id, &sign_pk) {
if id == peer_id {
let their_pk_b = box_::PublicKey(their_pk_b);
@@ -396,8 +416,8 @@ impl Client {
let sealed_key = box_::seal(&key.0, &nonce, &their_pk_b, &out_sk_b);
let mut msg_out = Message::new();
msg_out.set_public_key(PublicKey {
- asymmetric_value: our_pk_b.0.into(),
- symmetric_value: sealed_key,
+ asymmetric_value: Vec::from(our_pk_b.0).into(),
+ symmetric_value: sealed_key.into(),
..Default::default()
});
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
@@ -470,7 +490,7 @@ impl Client {
socket.send(&msg_out).await?;
if let Some(Ok(bytes)) = socket.next_timeout(CONNECT_TIMEOUT).await {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
- if let Some(rendezvous_message::Union::relay_response(rs)) = msg_in.union {
+ if let Some(rendezvous_message::Union::RelayResponse(rs)) = msg_in.union {
if !rs.refuse_reason.is_empty() {
bail!(rs.refuse_reason);
}
@@ -736,7 +756,12 @@ 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(),
+ decoder: Decoder::new(DecoderCfg {
+ vpx: VpxDecoderConfig {
+ codec: VpxVideoCodecId::VP9,
+ num_threads: (num_cpus::get() / 2) as _,
+ },
+ }),
latency_controller,
rgb: Default::default(),
}
@@ -752,35 +777,40 @@ impl VideoHandler {
.update_video(vf.timestamp);
}
match &vf.union {
- Some(video_frame::Union::vp9s(vp9s)) => self.handle_vp9s(vp9s),
+ Some(frame) => self.decoder.handle_video_frame(frame, &mut self.rgb),
_ => Ok(false),
}
}
/// 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() {
- for frame in self.decoder.decode(&vp9.data)? {
- drop(last_frame);
- last_frame = frame;
- }
- }
- for frame in self.decoder.flush()? {
- drop(last_frame);
- last_frame = frame;
- }
- if last_frame.is_null() {
- Ok(false)
- } else {
- last_frame.rgb(1, true, &mut self.rgb);
- Ok(true)
- }
- }
+ // pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType {
+ // let mut last_frame = Image::new();
+ // for vp9 in vp9s.frames.iter() {
+ // for frame in self.decoder.decode(&vp9.data)? {
+ // drop(last_frame);
+ // last_frame = frame;
+ // }
+ // }
+ // for frame in self.decoder.flush()? {
+ // drop(last_frame);
+ // last_frame = frame;
+ // }
+ // if last_frame.is_null() {
+ // Ok(false)
+ // } else {
+ // last_frame.rgb(1, true, &mut self.rgb);
+ // Ok(true)
+ // }
+ // }
/// Reset the decoder.
pub fn reset(&mut self) {
- self.decoder = Decoder::new(VideoCodecId::VP9, 1).unwrap();
+ self.decoder = Decoder::new(DecoderCfg {
+ vpx: VpxDecoderConfig {
+ codec: VpxVideoCodecId::VP9,
+ num_threads: 1,
+ },
+ });
}
}
@@ -798,6 +828,9 @@ pub struct LoginConfigHandler {
pub version: i64,
pub conn_id: i32,
features: Option,
+ session_id: u64,
+ pub supported_encoding: Option<(bool, bool)>,
+ pub restarting_remote_device: bool,
}
impl Deref for LoginConfigHandler {
@@ -833,6 +866,9 @@ impl LoginConfigHandler {
let config = self.load_config();
self.remember = !config.password.is_empty();
self.config = config;
+ self.session_id = rand::random();
+ self.supported_encoding = None;
+ self.restarting_remote_device = false;
}
/// Check if the client should auto login.
@@ -946,6 +982,8 @@ impl LoginConfigHandler {
option.block_input = BoolOption::Yes.into();
} else if name == "unblock-input" {
option.block_input = BoolOption::No.into();
+ } else if name == "show-quality-monitor" {
+ config.show_quality_monitor = !config.show_quality_monitor;
} else {
let v = self.options.get(&name).is_some();
if v {
@@ -984,15 +1022,8 @@ impl LoginConfigHandler {
n += 1;
} else if q == "custom" {
let config = PeerConfig::load(&self.id);
- let mut it = config.custom_image_quality.iter();
- let bitrate = it.next();
- let quantizer = it.next();
- if let Some(bitrate) = bitrate {
- if let Some(quantizer) = quantizer {
- msg.custom_image_quality = bitrate << 8 | quantizer;
- n += 1;
- }
- }
+ msg.custom_image_quality = config.custom_image_quality[0] << 8;
+ n += 1;
}
if self.get_toggle_option("show-remote-cursor") {
msg.show_remote_cursor = BoolOption::Yes.into();
@@ -1018,6 +1049,10 @@ impl LoginConfigHandler {
msg.disable_clipboard = BoolOption::Yes.into();
n += 1;
}
+ let state = Decoder::video_codec_state(&self.id);
+ msg.video_codec_state = hbb_common::protobuf::MessageField::some(state);
+ n += 1;
+
if n > 0 {
Some(msg)
} else {
@@ -1066,6 +1101,8 @@ impl LoginConfigHandler {
self.config.disable_audio
} else if name == "disable-clipboard" {
self.config.disable_clipboard
+ } else if name == "show-quality-monitor" {
+ self.config.show_quality_monitor
} else {
!self.get_option(name).is_empty()
}
@@ -1094,17 +1131,17 @@ impl LoginConfigHandler {
///
/// * `bitrate` - The given bitrate.
/// * `quantizer` - The given quantizer.
- pub fn save_custom_image_quality(&mut self, bitrate: i32, quantizer: i32) -> Message {
+ pub fn save_custom_image_quality(&mut self, image_quality: i32) -> Message {
let mut misc = Misc::new();
misc.set_option(OptionMessage {
- custom_image_quality: bitrate << 8 | quantizer,
+ custom_image_quality: image_quality << 8,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_misc(misc);
let mut config = self.load_config();
config.image_quality = "custom".to_owned();
- config.custom_image_quality = vec![bitrate, quantizer];
+ config.custom_image_quality = vec![image_quality as _];
self.save_config(config);
msg_out
}
@@ -1202,6 +1239,10 @@ impl LoginConfigHandler {
self.conn_id = pi.conn_id;
// no matter if change, for update file time
self.save_config(config);
+ #[cfg(feature = "hwcodec")]
+ {
+ self.supported_encoding = Some((pi.encoding.h264, pi.encoding.h265));
+ }
}
pub fn get_remote_dir(&self) -> String {
@@ -1231,10 +1272,11 @@ impl LoginConfigHandler {
let my_id = Config::get_id();
let mut lr = LoginRequest {
username: self.id.clone(),
- password,
+ password: password.into(),
my_id,
my_name: crate::username(),
option: self.get_option_message(true).into(),
+ session_id: self.session_id,
..Default::default()
};
if self.is_file_transfer {
@@ -1254,6 +1296,26 @@ impl LoginConfigHandler {
msg_out.set_login_request(lr);
msg_out
}
+
+ pub fn change_prefer_codec(&self) -> Message {
+ let state = scrap::codec::Decoder::video_codec_state(&self.id);
+ let mut misc = Misc::new();
+ misc.set_option(OptionMessage {
+ video_codec_state: hbb_common::protobuf::MessageField::some(state),
+ ..Default::default()
+ });
+ let mut msg_out = Message::new();
+ msg_out.set_misc(misc);
+ msg_out
+ }
+
+ pub fn restart_remote_device(&self) -> Message {
+ let mut misc = Misc::new();
+ misc.set_restart_remote_device(true);
+ let mut msg_out = Message::new();
+ msg_out.set_misc(misc);
+ msg_out
+ }
}
/// Media data.
@@ -1282,9 +1344,11 @@ where
let latency_controller = LatencyController::new();
let latency_controller_cl = latency_controller.clone();
+ // Create video_handler out of the thread below to ensure that the handler exists before client start.
+ // It will take a few tenths of a second for the first time, and then tens of milliseconds.
+ let mut video_handler = VideoHandler::new(latency_controller);
std::thread::spawn(move || {
- let mut video_handler = VideoHandler::new(latency_controller);
loop {
if let Ok(data) = video_receiver.recv() {
match data {
@@ -1459,11 +1523,21 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) {
/// * `peer` - [`Stream`] for communicating with peer.
pub async fn handle_hash(
lc: Arc>,
+ password_preset: &str,
hash: Hash,
interface: &impl Interface,
peer: &mut Stream,
) {
let mut password = lc.read().unwrap().password.clone();
+ if password.is_empty() {
+ if !password_preset.is_empty() {
+ let mut hasher = Sha256::new();
+ hasher.update(password_preset);
+ hasher.update(&hash.salt);
+ let res = hasher.finalize();
+ password = res[..].into();
+ }
+ }
if password.is_empty() {
password = lc.read().unwrap().config.password.clone();
}
@@ -1525,7 +1599,7 @@ pub trait Interface: Send + Clone + 'static + Sized {
fn msgbox(&self, msgtype: &str, title: &str, text: &str);
fn handle_login_error(&mut self, err: &str) -> bool;
fn handle_peer_info(&mut self, pi: PeerInfo);
- async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream);
+ async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream);
async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream);
async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream);
}
diff --git a/src/client/helper.rs b/src/client/helper.rs
index b29930e1c..b3ab6cb48 100644
--- a/src/client/helper.rs
+++ b/src/client/helper.rs
@@ -3,7 +3,7 @@ use std::{
time::Instant,
};
-use hbb_common::log;
+use hbb_common::{log, message_proto::{VideoFrame, video_frame}};
const MAX_LATENCY: i64 = 500;
const MIN_LATENCY: i64 = 100;
@@ -59,3 +59,33 @@ impl LatencyController {
self.allow_audio
}
}
+
+#[derive(PartialEq, Debug, Clone)]
+pub enum CodecFormat {
+ VP9,
+ H264,
+ H265,
+ Unknown,
+}
+
+impl From<&VideoFrame> for CodecFormat {
+ fn from(it: &VideoFrame) -> Self {
+ match it.union {
+ Some(video_frame::Union::Vp9s(_)) => CodecFormat::VP9,
+ Some(video_frame::Union::H264s(_)) => CodecFormat::H264,
+ Some(video_frame::Union::H265s(_)) => CodecFormat::H265,
+ _ => CodecFormat::Unknown,
+ }
+ }
+}
+
+impl ToString for CodecFormat {
+ fn to_string(&self) -> String {
+ match self {
+ CodecFormat::VP9 => "VP9".into(),
+ CodecFormat::H264 => "H264".into(),
+ CodecFormat::H265 => "H265".into(),
+ CodecFormat::Unknown => "Unknow".into(),
+ }
+ }
+}
diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs
index 39b2eb766..b6c0513e8 100644
--- a/src/clipboard_file.rs
+++ b/src/clipboard_file.rs
@@ -17,8 +17,8 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
});
}
Message {
- union: Some(message::Union::cliprdr(Cliprdr {
- union: Some(cliprdr::Union::format_list(CliprdrServerFormatList {
+ union: Some(message::Union::Cliprdr(Cliprdr {
+ union: Some(cliprdr::Union::FormatList(CliprdrServerFormatList {
conn_id,
formats,
..Default::default()
@@ -29,8 +29,8 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
}
}
ClipbaordFile::ServerFormatListResponse { conn_id, msg_flags } => Message {
- union: Some(message::Union::cliprdr(Cliprdr {
- union: Some(cliprdr::Union::format_list_response(
+ union: Some(message::Union::Cliprdr(Cliprdr {
+ union: Some(cliprdr::Union::FormatListResponse(
CliprdrServerFormatListResponse {
conn_id,
msg_flags,
@@ -45,8 +45,8 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
conn_id,
requested_format_id,
} => Message {
- union: Some(message::Union::cliprdr(Cliprdr {
- union: Some(cliprdr::Union::format_data_request(
+ union: Some(message::Union::Cliprdr(Cliprdr {
+ union: Some(cliprdr::Union::FormatDataRequest(
CliprdrServerFormatDataRequest {
conn_id,
requested_format_id,
@@ -62,12 +62,12 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
msg_flags,
format_data,
} => Message {
- union: Some(message::Union::cliprdr(Cliprdr {
- union: Some(cliprdr::Union::format_data_response(
+ union: Some(message::Union::Cliprdr(Cliprdr {
+ union: Some(cliprdr::Union::FormatDataResponse(
CliprdrServerFormatDataResponse {
conn_id,
msg_flags,
- format_data,
+ format_data: format_data.into(),
..Default::default()
},
)),
@@ -86,8 +86,8 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
have_clip_data_id,
clip_data_id,
} => Message {
- union: Some(message::Union::cliprdr(Cliprdr {
- union: Some(cliprdr::Union::file_contents_request(
+ union: Some(message::Union::Cliprdr(Cliprdr {
+ union: Some(cliprdr::Union::FileContentsRequest(
CliprdrFileContentsRequest {
conn_id,
stream_id,
@@ -111,13 +111,13 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
stream_id,
requested_data,
} => Message {
- union: Some(message::Union::cliprdr(Cliprdr {
- union: Some(cliprdr::Union::file_contents_response(
+ union: Some(message::Union::Cliprdr(Cliprdr {
+ union: Some(cliprdr::Union::FileContentsResponse(
CliprdrFileContentsResponse {
conn_id,
msg_flags,
stream_id,
- requested_data,
+ requested_data: requested_data.into(),
..Default::default()
},
)),
@@ -130,7 +130,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
pub fn msg_2_clip(msg: Cliprdr) -> Option {
match msg.union {
- Some(cliprdr::Union::format_list(data)) => {
+ Some(cliprdr::Union::FormatList(data)) => {
let mut format_list: Vec<(i32, String)> = Vec::new();
for v in data.formats.iter() {
format_list.push((v.id, v.format.clone()));
@@ -140,26 +140,26 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option {
format_list,
})
}
- Some(cliprdr::Union::format_list_response(data)) => {
+ Some(cliprdr::Union::FormatListResponse(data)) => {
Some(ClipbaordFile::ServerFormatListResponse {
conn_id: data.conn_id,
msg_flags: data.msg_flags,
})
}
- Some(cliprdr::Union::format_data_request(data)) => {
+ Some(cliprdr::Union::FormatDataRequest(data)) => {
Some(ClipbaordFile::ServerFormatDataRequest {
conn_id: data.conn_id,
requested_format_id: data.requested_format_id,
})
}
- Some(cliprdr::Union::format_data_response(data)) => {
+ Some(cliprdr::Union::FormatDataResponse(data)) => {
Some(ClipbaordFile::ServerFormatDataResponse {
conn_id: data.conn_id,
msg_flags: data.msg_flags,
- format_data: data.format_data,
+ format_data: data.format_data.into(),
})
}
- Some(cliprdr::Union::file_contents_request(data)) => {
+ Some(cliprdr::Union::FileContentsRequest(data)) => {
Some(ClipbaordFile::FileContentsRequest {
conn_id: data.conn_id,
stream_id: data.stream_id,
@@ -172,12 +172,12 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option {
clip_data_id: data.clip_data_id,
})
}
- Some(cliprdr::Union::file_contents_response(data)) => {
+ Some(cliprdr::Union::FileContentsResponse(data)) => {
Some(ClipbaordFile::FileContentsResponse {
conn_id: data.conn_id,
msg_flags: data.msg_flags,
stream_id: data.stream_id,
- requested_data: data.requested_data,
+ requested_data: data.requested_data.into(),
})
}
_ => None,
diff --git a/src/common.rs b/src/common.rs
index c344b93a1..d2d1922ec 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -11,8 +11,8 @@ use hbb_common::{
config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT},
get_version_number, log,
message_proto::*,
+ protobuf::Enum,
protobuf::Message as _,
- protobuf::ProtobufEnum,
rendezvous_proto::*,
sleep, socket_client, tokio, ResultType,
};
@@ -34,7 +34,7 @@ lazy_static::lazy_static! {
#[inline]
pub fn valid_for_numlock(evt: &KeyEvent) -> bool {
- if let Some(key_event::Union::control_key(ck)) = evt.union {
+ if let Some(key_event::Union::ControlKey(ck)) = evt.union {
let v = ck.value();
(v >= ControlKey::Numpad0.value() && v <= ControlKey::Numpad9.value())
|| v == ControlKey::Decimal.value()
@@ -51,7 +51,7 @@ pub fn create_clipboard_msg(content: String) -> Message {
let mut msg = Message::new();
msg.set_clipboard(Clipboard {
compress,
- content,
+ content: content.into(),
..Default::default()
});
msg
@@ -82,7 +82,7 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>)
let content = if clipboard.compress {
decompress(&clipboard.content)
} else {
- clipboard.content
+ clipboard.content.into()
};
if let Ok(content) = String::from_utf8(content) {
if content.is_empty() {
@@ -249,7 +249,7 @@ async fn test_nat_type_() -> ResultType {
return Ok(true);
}
let start = std::time::Instant::now();
- let rendezvous_server = get_rendezvous_server(1_000).await;
+ let (rendezvous_server, _, _) = get_rendezvous_server(1_000).await;
let server1 = rendezvous_server;
let tmp: Vec<&str> = server1.split(":").collect();
if tmp.len() != 2 {
@@ -286,7 +286,7 @@ async fn test_nat_type_() -> ResultType {
socket.send(&msg_out).await?;
if let Some(Ok(bytes)) = socket.next_timeout(RENDEZVOUS_TIMEOUT).await {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
- if let Some(rendezvous_message::Union::test_nat_response(tnr)) = msg_in.union {
+ if let Some(rendezvous_message::Union::TestNatResponse(tnr)) = msg_in.union {
if i == 0 {
port1 = tnr.port;
} else {
@@ -318,21 +318,53 @@ async fn test_nat_type_() -> ResultType {
Ok(ok)
}
-#[cfg(any(target_os = "android", target_os = "ios"))]
-pub async fn get_rendezvous_server(_ms_timeout: u64) -> String {
- Config::get_rendezvous_server()
+pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec, bool) {
+ #[cfg(any(target_os = "android", target_os = "ios"))]
+ let (mut a, mut b) = get_rendezvous_server_(ms_timeout);
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ let (mut a, mut b) = get_rendezvous_server_(ms_timeout).await;
+ let mut b: Vec = b
+ .drain(..)
+ .map(|x| {
+ if !x.contains(":") {
+ format!("{}:{}", x, config::RENDEZVOUS_PORT)
+ } else {
+ x
+ }
+ })
+ .collect();
+ let c = if b.contains(&a) {
+ b = b.drain(..).filter(|x| x != &a).collect();
+ true
+ } else {
+ a = b.pop().unwrap_or(a);
+ false
+ };
+ (a, b, c)
}
+#[inline]
+#[cfg(any(target_os = "android", target_os = "ios"))]
+fn get_rendezvous_server_(_ms_timeout: u64) -> (String, Vec) {
+ (
+ Config::get_rendezvous_server(),
+ Config::get_rendezvous_servers(),
+ )
+}
+
+#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
-pub async fn get_rendezvous_server(ms_timeout: u64) -> String {
+async fn get_rendezvous_server_(ms_timeout: u64) -> (String, Vec) {
crate::ipc::get_rendezvous_server(ms_timeout).await
}
+#[inline]
#[cfg(any(target_os = "android", target_os = "ios"))]
pub async fn get_nat_type(_ms_timeout: u64) -> i32 {
Config::get_nat_type()
}
+#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub async fn get_nat_type(ms_timeout: u64) -> i32 {
crate::ipc::get_nat_type(ms_timeout).await
@@ -342,7 +374,7 @@ pub async fn get_nat_type(ms_timeout: u64) -> i32 {
#[tokio::main(flavor = "current_thread")]
async fn test_rendezvous_server_() {
let servers = Config::get_rendezvous_servers();
- hbb_common::config::ONLINE.lock().unwrap().clear();
+ Config::reset_online();
let mut futs = Vec::new();
for host in servers {
futs.push(tokio::spawn(async move {
@@ -370,6 +402,17 @@ pub fn test_rendezvous_server() {
std::thread::spawn(test_rendezvous_server_);
}
+pub fn refresh_rendezvous_server() {
+ #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
+ test_rendezvous_server();
+ #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
+ std::thread::spawn(|| {
+ if crate::ipc::test_rendezvous_server().is_err() {
+ test_rendezvous_server();
+ }
+ });
+}
+
#[inline]
pub fn get_time() -> i64 {
std::time::SystemTime::now()
@@ -414,7 +457,7 @@ pub const POSTFIX_SERVICE: &'static str = "_service";
#[inline]
pub fn is_control_key(evt: &KeyEvent, key: &ControlKey) -> bool {
- if let Some(key_event::Union::control_key(ck)) = evt.union {
+ if let Some(key_event::Union::ControlKey(ck)) = evt.union {
ck.value() == key.value()
} else {
false
@@ -423,7 +466,7 @@ pub fn is_control_key(evt: &KeyEvent, key: &ControlKey) -> bool {
#[inline]
pub fn is_modifier(evt: &KeyEvent) -> bool {
- if let Some(key_event::Union::control_key(ck)) = evt.union {
+ if let Some(key_event::Union::ControlKey(ck)) = evt.union {
let v = ck.value();
v == ControlKey::Alt.value()
|| v == ControlKey::Shift.value()
@@ -439,14 +482,15 @@ pub fn is_modifier(evt: &KeyEvent) -> bool {
}
pub fn check_software_update() {
- std::thread::spawn(move || allow_err!(_check_software_update()));
+ std::thread::spawn(move || allow_err!(check_software_update_()));
}
#[tokio::main(flavor = "current_thread")]
-async fn _check_software_update() -> hbb_common::ResultType<()> {
+async fn check_software_update_() -> hbb_common::ResultType<()> {
sleep(3.).await;
- let rendezvous_server = socket_client::get_target_addr(&get_rendezvous_server(1_000).await)?;
+ let rendezvous_server =
+ socket_client::get_target_addr(&format!("rs-sg.rustdesk.com:{}", config::RENDEZVOUS_PORT))?;
let mut socket =
socket_client::new_udp(Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT).await?;
@@ -459,7 +503,7 @@ async fn _check_software_update() -> hbb_common::ResultType<()> {
use hbb_common::protobuf::Message;
if let Some(Ok((bytes, _))) = socket.next_timeout(30_000).await {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
- if let Some(rendezvous_message::Union::software_update(su)) = msg_in.union {
+ if let Some(rendezvous_message::Union::SoftwareUpdate(su)) = msg_in.union {
let version = hbb_common::get_version_from_url(&su.url);
if get_version_number(&version) > get_version_number(crate::VERSION) {
*SOFTWARE_UPDATE_URL.lock().unwrap() = su.url;
@@ -498,14 +542,6 @@ pub fn is_setup(name: &str) -> bool {
name.to_lowercase().ends_with("setdown.exe") || name.to_lowercase().ends_with("安装.exe")
}
-pub fn get_uuid() -> Vec {
- #[cfg(not(any(target_os = "android", target_os = "ios")))]
- if let Ok(id) = machine_uid::get() {
- return id.into();
- }
- Config::get_key_pair().1
-}
-
pub fn get_custom_rendezvous_server(custom: String) -> String {
if !custom.is_empty() {
return custom;
diff --git a/src/flutter.rs b/src/flutter.rs
index 2807d1711..edd972f68 100644
--- a/src/flutter.rs
+++ b/src/flutter.rs
@@ -590,8 +590,8 @@ impl Interface for Session {
}
}
- async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream) {
- handle_hash(self.lc.clone(), hash, self, peer).await;
+ async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) {
+ handle_hash(self.lc.clone(), pass, hash, self, peer).await;
}
async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) {
@@ -709,7 +709,6 @@ impl Connection {
log::debug!("Exit io_loop of id={}", session.id);
}
Err(err) => {
- crate::common::test_rendezvous_server();
session.msgbox("error", "Connection Error", &err.to_string());
}
}
@@ -722,7 +721,7 @@ impl Connection {
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 {
- Some(message::Union::video_frame(vf)) => {
+ Some(message::Union::VideoFrame(vf)) => {
if !self.first_frame {
self.first_frame = true;
}
@@ -733,26 +732,26 @@ impl Connection {
)));
}
}
- Some(message::Union::hash(hash)) => {
- self.session.handle_hash(hash, peer).await;
+ Some(message::Union::Hash(hash)) => {
+ self.session.handle_hash("", hash, peer).await;
}
- Some(message::Union::login_response(lr)) => match lr.union {
- Some(login_response::Union::error(err)) => {
+ Some(message::Union::LoginResponse(lr)) => match lr.union {
+ Some(login_response::Union::Error(err)) => {
if !self.session.handle_login_error(&err) {
return false;
}
}
- Some(login_response::Union::peer_info(pi)) => {
+ Some(login_response::Union::PeerInfo(pi)) => {
self.session.handle_peer_info(pi);
}
_ => {}
},
- Some(message::Union::clipboard(cb)) => {
+ Some(message::Union::Clipboard(cb)) => {
if !self.session.lc.read().unwrap().disable_clipboard {
let content = if cb.compress {
decompress(&cb.content)
} else {
- cb.content
+ cb.content.into()
};
if let Ok(content) = String::from_utf8(content) {
self.session
@@ -760,7 +759,7 @@ impl Connection {
}
}
}
- Some(message::Union::cursor_data(cd)) => {
+ Some(message::Union::CursorData(cd)) => {
let colors = hbb_common::compress::decompress(&cd.colors);
self.session.push_event(
"cursor_data",
@@ -777,18 +776,18 @@ impl Connection {
],
);
}
- Some(message::Union::cursor_id(id)) => {
+ Some(message::Union::CursorId(id)) => {
self.session
.push_event("cursor_id", vec![("id", &id.to_string())]);
}
- Some(message::Union::cursor_position(cp)) => {
+ Some(message::Union::CursorPosition(cp)) => {
self.session.push_event(
"cursor_position",
vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())],
);
}
- Some(message::Union::file_response(fr)) => match fr.union {
- Some(file_response::Union::dir(fd)) => {
+ Some(message::Union::FileResponse(fr)) => match fr.union {
+ Some(file_response::Union::Dir(fd)) => {
let mut entries = fd.entries.to_vec();
if self.session.peer_platform() == "Windows" {
fs::transform_windows_path(&mut entries);
@@ -802,7 +801,7 @@ impl Connection {
job.set_files(entries);
}
}
- Some(file_response::Union::block(block)) => {
+ Some(file_response::Union::Block(block)) => {
if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) {
if let Err(_err) = job.write(block, None).await {
// to-do: add "skip" for writing job
@@ -810,17 +809,17 @@ impl Connection {
self.update_jobs_status();
}
}
- Some(file_response::Union::done(d)) => {
+ Some(file_response::Union::Done(d)) => {
if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) {
job.modify_time();
fs::remove_job(d.id, &mut self.write_jobs);
}
self.handle_job_status(d.id, d.file_num, None);
}
- Some(file_response::Union::error(e)) => {
+ Some(file_response::Union::Error(e)) => {
self.handle_job_status(e.id, e.file_num, Some(e.error));
}
- Some(file_response::Union::digest(digest)) => {
+ Some(file_response::Union::Digest(digest)) => {
if digest.is_upload {
if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) {
if let Some(file) = job.files().get(digest.file_num as usize) {
@@ -831,9 +830,9 @@ impl Connection {
id: digest.id,
file_num: digest.file_num,
union: Some(if overwrite {
- file_transfer_send_confirm_request::Union::offset_blk(0)
+ file_transfer_send_confirm_request::Union::OffsetBlk(0)
} else {
- file_transfer_send_confirm_request::Union::skip(
+ file_transfer_send_confirm_request::Union::Skip(
true,
)
}),
@@ -863,7 +862,7 @@ impl Connection {
let msg= new_send_confirm(FileTransferSendConfirmRequest {
id: digest.id,
file_num: digest.file_num,
- union: Some(file_transfer_send_confirm_request::Union::skip(true)),
+ union: Some(file_transfer_send_confirm_request::Union::Skip(true)),
..Default::default()
});
self.session.send_msg(msg);
@@ -875,9 +874,9 @@ impl Connection {
id: digest.id,
file_num: digest.file_num,
union: Some(if overwrite {
- file_transfer_send_confirm_request::Union::offset_blk(0)
+ file_transfer_send_confirm_request::Union::OffsetBlk(0)
} else {
- file_transfer_send_confirm_request::Union::skip(true)
+ file_transfer_send_confirm_request::Union::Skip(true)
}),
..Default::default()
},
@@ -897,7 +896,7 @@ impl Connection {
FileTransferSendConfirmRequest {
id: digest.id,
file_num: digest.file_num,
- union: Some(file_transfer_send_confirm_request::Union::offset_blk(0)),
+ union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)),
..Default::default()
},
);
@@ -914,15 +913,15 @@ impl Connection {
}
_ => {}
},
- Some(message::Union::misc(misc)) => match misc.union {
- Some(misc::Union::audio_format(f)) => {
+ Some(message::Union::Misc(misc)) => match misc.union {
+ Some(misc::Union::AudioFormat(f)) => {
self.audio_handler.handle_format(f); //
}
- Some(misc::Union::chat_message(c)) => {
+ Some(misc::Union::ChatMessage(c)) => {
self.session
.push_event("chat_client_mode", vec![("text", &c.text)]);
}
- Some(misc::Union::permission_info(p)) => {
+ Some(misc::Union::PermissionInfo(p)) => {
log::info!("Change permission {:?} -> {}", p.permission, p.enabled);
use permission_info::Permission;
self.session.push_event(
@@ -938,7 +937,7 @@ impl Connection {
)],
);
}
- Some(misc::Union::switch_display(s)) => {
+ Some(misc::Union::SwitchDisplay(s)) => {
self.video_handler.reset();
self.session.push_event(
"switch_display",
@@ -951,22 +950,22 @@ impl Connection {
],
);
}
- Some(misc::Union::close_reason(c)) => {
+ Some(misc::Union::CloseReason(c)) => {
self.session.msgbox("error", "Connection Error", &c);
return false;
}
_ => {}
},
- Some(message::Union::test_delay(t)) => {
+ Some(message::Union::TestDelay(t)) => {
self.session.handle_test_delay(t, peer).await;
}
- Some(message::Union::audio_frame(frame)) => {
+ Some(message::Union::AudioFrame(frame)) => {
if !self.session.lc.read().unwrap().disable_audio {
self.audio_handler.handle_frame(frame);
}
}
- Some(message::Union::file_action(action)) => match action.union {
- Some(file_action::Union::send_confirm(c)) => {
+ Some(message::Union::FileAction(action)) => match action.union {
+ Some(file_action::Union::SendConfirm(c)) => {
if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) {
job.confirm(&c);
}
@@ -1155,9 +1154,9 @@ impl Connection {
id,
file_num,
union: if need_override {
- Some(file_transfer_send_confirm_request::Union::offset_blk(0))
+ Some(file_transfer_send_confirm_request::Union::OffsetBlk(0))
} else {
- Some(file_transfer_send_confirm_request::Union::skip(true))
+ Some(file_transfer_send_confirm_request::Union::Skip(true))
},
..Default::default()
});
@@ -1173,9 +1172,9 @@ impl Connection {
id,
file_num,
union: if need_override {
- Some(file_transfer_send_confirm_request::Union::offset_blk(0))
+ Some(file_transfer_send_confirm_request::Union::OffsetBlk(0))
} else {
- Some(file_transfer_send_confirm_request::Union::skip(true))
+ Some(file_transfer_send_confirm_request::Union::Skip(true))
},
..Default::default()
});
@@ -1441,15 +1440,13 @@ pub mod connection_manager {
Some(Data::Login {
id,
is_file_transfer,
- port_forward,
peer_id,
name,
authorized,
keyboard,
clipboard,
audio,
- file,
- file_transfer_enabled,
+ ..
}) => {
current_id = id;
let mut client = Client {
@@ -1681,7 +1678,7 @@ pub mod connection_manager {
let mut req = FileTransferSendConfirmRequest {
id,
file_num,
- union: Some(file_transfer_send_confirm_request::Union::offset_blk(0)),
+ union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)),
..Default::default()
};
let digest = FileTransferDigest {
diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs
index afbe35ec8..f1aeabfcc 100644
--- a/src/flutter_ffi.rs
+++ b/src/flutter_ffi.rs
@@ -7,7 +7,7 @@ use std::{
use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer};
use serde_json::{json, Number, Value};
-use hbb_common::ResultType;
+use hbb_common::{ResultType, password_security};
use hbb_common::{
config::{self, Config, LocalConfig, PeerConfig, ONLINE},
fs, log,
@@ -24,7 +24,8 @@ use crate::ui_interface::{
get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_license,
get_local_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs,
get_uuid, get_version, has_rendezvous_service, is_ok_change_id, post_request, set_local_option,
- set_options, set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server,
+ set_options, set_peer_option, set_socks, store_fav, temporary_password, test_if_valid_server,
+ using_public_server,
};
fn initialize(app_dir: &str) {
@@ -581,8 +582,8 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co
"server_id" => {
res = ui_interface::get_id();
}
- "server_password" => {
- res = Config::get_password();
+ "temporary_password" => {
+ res = password_security::temporary_password();
}
"connect_statue" => {
res = ONLINE
@@ -627,7 +628,7 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co
}
}
"uuid" => {
- res = base64::encode(crate::get_uuid());
+ res = base64::encode(get_uuid());
}
_ => {
log::error!("Unknown name of get_by_name: {}", name);
@@ -942,13 +943,13 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) {
// }
// }
// Server Side
- "update_password" => {
- if value.is_empty() {
- Config::set_password(&Config::get_auto_password());
- } else {
- Config::set_password(value);
- }
- }
+ // "update_password" => {
+ // if value.is_empty() {
+ // Config::set_password(&Config::get_auto_password());
+ // } else {
+ // Config::set_password(value);
+ // }
+ // }
#[cfg(target_os = "android")]
"chat_server_mode" => {
if let Ok(m) = serde_json::from_str::>(value) {
diff --git a/src/ipc.rs b/src/ipc.rs
index c20864700..99670890e 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -2,6 +2,7 @@ use std::{collections::HashMap, sync::atomic::Ordering};
#[cfg(not(windows))]
use std::{fs::File, io::prelude::*};
+use bytes::Bytes;
use parity_tokio_ipc::{
Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes,
};
@@ -15,7 +16,7 @@ use hbb_common::{
config::{self, Config, Config2},
futures::StreamExt as _,
futures_util::sink::SinkExt,
- log, timeout, tokio,
+ log, password_security as password, timeout, tokio,
tokio::io::{AsyncRead, AsyncWrite},
tokio_util::codec::Framed,
ResultType,
@@ -66,7 +67,7 @@ pub enum FS {
WriteBlock {
id: i32,
file_num: i32,
- data: Vec,
+ data: Bytes,
compressed: bool,
},
WriteDone {
@@ -87,6 +88,47 @@ pub enum FS {
},
}
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(tag = "t", content = "c")]
+pub enum DataKeyboard {
+ Sequence(String),
+ KeyDown(enigo::Key),
+ KeyUp(enigo::Key),
+ KeyClick(enigo::Key),
+ GetKeyState(enigo::Key),
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(tag = "t", content = "c")]
+pub enum DataKeyboardResponse {
+ GetKeyState(bool),
+}
+
+#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
+#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(tag = "t", content = "c")]
+pub enum DataMouse {
+ MoveTo(i32, i32),
+ MoveRelative(i32, i32),
+ Down(enigo::MouseButton),
+ Up(enigo::MouseButton),
+ Click(enigo::MouseButton),
+ ScrollX(i32),
+ ScrollY(i32),
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(tag = "t", content = "c")]
+pub enum DataControl {
+ Resolution {
+ minx: i32,
+ maxx: i32,
+ miny: i32,
+ maxy: i32,
+ },
+}
+
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum Data {
@@ -102,6 +144,7 @@ pub enum Data {
audio: bool,
file: bool,
file_transfer_enabled: bool,
+ restart: bool,
},
ChatMessage {
text: String,
@@ -130,6 +173,15 @@ pub enum Data {
ClipbaordFile(ClipbaordFile),
ClipboardFileEnabled(bool),
PrivacyModeState((i32, PrivacyModeState)),
+ TestRendezvousServer,
+ #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
+ Keyboard(DataKeyboard),
+ #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
+ KeyboardResponse(DataKeyboardResponse),
+ #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
+ Mouse(DataMouse),
+ Control(DataControl),
+ Empty,
}
#[tokio::main(flavor = "current_thread")]
@@ -284,12 +336,18 @@ async fn handle(data: Data, stream: &mut Connection) {
let value;
if name == "id" {
value = Some(Config::get_id());
- } else if name == "password" {
- value = Some(Config::get_password());
+ } else if name == "temporary-password" {
+ value = Some(password::temporary_password());
+ } else if name == "permanent-password" {
+ value = Some(Config::get_permanent_password());
} else if name == "salt" {
value = Some(Config::get_salt());
} else if name == "rendezvous_server" {
- value = Some(Config::get_rendezvous_server());
+ value = Some(format!(
+ "{},{}",
+ Config::get_rendezvous_server(),
+ Config::get_rendezvous_servers().join(",")
+ ));
} else if name == "rendezvous_servers" {
value = Some(Config::get_rendezvous_servers().join(","));
} else {
@@ -301,8 +359,10 @@ async fn handle(data: Data, stream: &mut Connection) {
if name == "id" {
Config::set_key_confirmed(false);
Config::set_id(&value);
- } else if name == "password" {
- Config::set_password(&value);
+ } else if name == "temporary-password" {
+ password::update_temporary_password();
+ } else if name == "permanent-password" {
+ Config::set_permanent_password(&value);
} else if name == "salt" {
Config::set_salt(&value);
} else {
@@ -339,6 +399,9 @@ async fn handle(data: Data, stream: &mut Connection) {
.await
);
}
+ Data::TestRendezvousServer => {
+ crate::test_rendezvous_server();
+ }
_ => {}
}
}
@@ -450,8 +513,8 @@ where
}
}
- pub async fn send_raw(&mut self, data: Vec) -> ResultType<()> {
- self.inner.send(bytes::Bytes::from(data)).await?;
+ pub async fn send_raw(&mut self, data: Bytes) -> ResultType<()> {
+ self.inner.send(data).await?;
Ok(())
}
@@ -492,9 +555,22 @@ pub async fn set_config(name: &str, value: String) -> ResultType<()> {
set_config_async(name, value).await
}
-pub fn set_password(v: String) -> ResultType<()> {
- Config::set_password(&v);
- set_config("password", v)
+pub fn update_temporary_password() -> ResultType<()> {
+ set_config("temporary-password", "".to_owned())
+}
+
+pub fn get_permanent_password() -> String {
+ if let Ok(Some(v)) = get_config("permanent-password") {
+ Config::set_permanent_password(&v);
+ v
+ } else {
+ Config::get_permanent_password()
+ }
+}
+
+pub fn set_permanent_password(v: String) -> ResultType<()> {
+ Config::set_permanent_password(&v);
+ set_config("permanent-password", v)
}
pub fn get_id() -> String {
@@ -513,20 +589,17 @@ pub fn get_id() -> String {
}
}
-pub fn get_password() -> String {
- if let Ok(Some(v)) = get_config("password") {
- Config::set_password(&v);
- v
- } else {
- Config::get_password()
- }
-}
-
-pub async fn get_rendezvous_server(ms_timeout: u64) -> String {
+pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec) {
if let Ok(Some(v)) = get_config_async("rendezvous_server", ms_timeout).await {
- v
+ let mut urls = v.split(",");
+ let a = urls.next().unwrap_or_default().to_owned();
+ let b: Vec = urls.map(|x| x.to_owned()).collect();
+ (a, b)
} else {
- Config::get_rendezvous_server()
+ (
+ Config::get_rendezvous_server(),
+ Config::get_rendezvous_servers(),
+ )
}
}
@@ -638,3 +711,10 @@ pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> {
.await?;
Ok(())
}
+
+#[tokio::main(flavor = "current_thread")]
+pub async fn test_rendezvous_server() -> ResultType<()> {
+ let mut c = connect(1000, "").await?;
+ c.send(&Data::TestRendezvousServer).await?;
+ Ok(())
+}
diff --git a/src/lan.rs b/src/lan.rs
new file mode 100644
index 000000000..733e271a9
--- /dev/null
+++ b/src/lan.rs
@@ -0,0 +1,291 @@
+use hbb_common::{
+ allow_err,
+ anyhow::bail,
+ config::{self, Config, RENDEZVOUS_PORT},
+ log,
+ protobuf::Message as _,
+ rendezvous_proto::*,
+ tokio::{
+ self,
+ sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
+ },
+ ResultType,
+};
+use std::{
+ collections::{HashMap, HashSet},
+ net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs, UdpSocket},
+ time::Instant,
+};
+
+type Message = RendezvousMessage;
+
+pub(super) fn start_listening() -> ResultType<()> {
+ let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port()));
+ let socket = std::net::UdpSocket::bind(addr)?;
+ socket.set_read_timeout(Some(std::time::Duration::from_millis(1000)))?;
+ log::info!("lan discovery listener started");
+ loop {
+ let mut buf = [0; 2048];
+ if let Ok((len, addr)) = socket.recv_from(&mut buf) {
+ if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) {
+ match msg_in.union {
+ Some(rendezvous_message::Union::PeerDiscovery(p)) => {
+ if p.cmd == "ping" {
+ if let Some(self_addr) = get_ipaddr_by_peer(&addr) {
+ let mut msg_out = Message::new();
+ let peer = PeerDiscovery {
+ cmd: "pong".to_owned(),
+ mac: get_mac(&self_addr),
+ id: Config::get_id(),
+ hostname: whoami::hostname(),
+ username: crate::platform::get_active_username(),
+ platform: whoami::platform().to_string(),
+ ..Default::default()
+ };
+ msg_out.set_peer_discovery(peer);
+ socket.send_to(&msg_out.write_to_bytes()?, addr).ok();
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+}
+
+#[tokio::main(flavor = "current_thread")]
+pub async fn discover() -> ResultType<()> {
+ let sockets = send_query()?;
+ let rx = spawn_wait_responses(sockets);
+ handle_received_peers(rx).await?;
+
+ log::info!("discover ping done");
+ Ok(())
+}
+
+pub fn send_wol(id: String) {
+ let interfaces = default_net::get_interfaces();
+ for peer in &config::LanPeers::load().peers {
+ if peer.id == id {
+ for (ip, mac) in peer.ip_mac.iter() {
+ if let Ok(mac_addr) = mac.parse() {
+ if let Ok(IpAddr::V4(ip)) = ip.parse() {
+ for interface in &interfaces {
+ for ipv4 in &interface.ipv4 {
+ if (u32::from(ipv4.addr) & u32::from(ipv4.netmask))
+ == (u32::from(ip) & u32::from(ipv4.netmask))
+ {
+ allow_err!(wol::send_wol(
+ mac_addr,
+ None,
+ Some(IpAddr::V4(ipv4.addr))
+ ));
+ }
+ }
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+}
+
+#[inline]
+fn get_broadcast_port() -> u16 {
+ (RENDEZVOUS_PORT + 3) as _
+}
+
+fn get_mac(ip: &IpAddr) -> String {
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ if let Ok(mac) = get_mac_by_ip(ip) {
+ mac.to_string()
+ } else {
+ "".to_owned()
+ }
+ #[cfg(any(target_os = "android", target_os = "ios"))]
+ "".to_owned()
+}
+
+fn get_all_ipv4s() -> ResultType> {
+ let mut ipv4s = Vec::new();
+ for interface in default_net::get_interfaces() {
+ for ipv4 in &interface.ipv4 {
+ ipv4s.push(ipv4.addr.clone());
+ }
+ }
+ Ok(ipv4s)
+}
+
+fn get_mac_by_ip(ip: &IpAddr) -> ResultType {
+ for interface in default_net::get_interfaces() {
+ match ip {
+ IpAddr::V4(local_ipv4) => {
+ if interface.ipv4.iter().any(|x| x.addr == *local_ipv4) {
+ if let Some(mac_addr) = interface.mac_addr {
+ return Ok(mac_addr.address());
+ }
+ }
+ }
+ IpAddr::V6(local_ipv6) => {
+ if interface.ipv6.iter().any(|x| x.addr == *local_ipv6) {
+ if let Some(mac_addr) = interface.mac_addr {
+ return Ok(mac_addr.address());
+ }
+ }
+ }
+ }
+ }
+ bail!("No interface found for ip: {:?}", ip);
+}
+
+// Mainly from https://github.com/shellrow/default-net/blob/cf7ca24e7e6e8e566ed32346c9cfddab3f47e2d6/src/interface/shared.rs#L4
+fn get_ipaddr_by_peer(peer: A) -> Option {
+ let socket = match UdpSocket::bind("0.0.0.0:0") {
+ Ok(s) => s,
+ Err(_) => return None,
+ };
+
+ match socket.connect(peer) {
+ Ok(()) => (),
+ Err(_) => return None,
+ };
+
+ match socket.local_addr() {
+ Ok(addr) => return Some(addr.ip()),
+ Err(_) => return None,
+ };
+}
+
+fn create_broadcast_sockets() -> ResultType> {
+ let mut sockets = Vec::new();
+ for v4_addr in get_all_ipv4s()? {
+ if v4_addr.is_private() {
+ let s = UdpSocket::bind(SocketAddr::from((v4_addr, 0)))?;
+ s.set_broadcast(true)?;
+ log::debug!("Bind socket to {}", &v4_addr);
+ sockets.push(s)
+ }
+ }
+ Ok(sockets)
+}
+
+fn send_query() -> ResultType> {
+ let sockets = create_broadcast_sockets()?;
+ if sockets.is_empty() {
+ bail!("Found no ipv4 addresses");
+ }
+
+ let mut msg_out = Message::new();
+ let peer = PeerDiscovery {
+ cmd: "ping".to_owned(),
+ ..Default::default()
+ };
+ msg_out.set_peer_discovery(peer);
+ let maddr = SocketAddr::from(([255, 255, 255, 255], get_broadcast_port()));
+ for socket in &sockets {
+ socket.send_to(&msg_out.write_to_bytes()?, maddr)?;
+ }
+ log::info!("discover ping sent");
+ Ok(sockets)
+}
+
+fn wait_response(
+ socket: UdpSocket,
+ timeout: Option,
+ tx: UnboundedSender,
+) -> ResultType<()> {
+ let mut last_recv_time = Instant::now();
+
+ socket.set_read_timeout(timeout)?;
+ loop {
+ let mut buf = [0; 2048];
+ if let Ok((len, addr)) = socket.recv_from(&mut buf) {
+ if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) {
+ match msg_in.union {
+ Some(rendezvous_message::Union::PeerDiscovery(p)) => {
+ last_recv_time = Instant::now();
+ if p.cmd == "pong" {
+ let mac = if let Some(self_addr) = get_ipaddr_by_peer(&addr) {
+ get_mac(&self_addr)
+ } else {
+ "".to_owned()
+ };
+
+ if mac != p.mac {
+ allow_err!(tx.send(config::DiscoveryPeer {
+ id: p.id.clone(),
+ ip_mac: HashMap::from([
+ (addr.ip().to_string(), p.mac.clone(),)
+ ]),
+ username: p.username.clone(),
+ hostname: p.hostname.clone(),
+ platform: p.platform.clone(),
+ online: true,
+ }));
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ if last_recv_time.elapsed().as_millis() > 3_000 {
+ break;
+ }
+ }
+ Ok(())
+}
+
+fn spawn_wait_responses(sockets: Vec) -> UnboundedReceiver {
+ let (tx, rx) = unbounded_channel::<_>();
+ for socket in sockets {
+ let tx_clone = tx.clone();
+ std::thread::spawn(move || {
+ allow_err!(wait_response(
+ socket,
+ Some(std::time::Duration::from_millis(10)),
+ tx_clone
+ ));
+ });
+ }
+ rx
+}
+
+async fn handle_received_peers(mut rx: UnboundedReceiver) -> ResultType<()> {
+ let mut peers = config::LanPeers::load().peers;
+ peers.iter_mut().for_each(|peer| {
+ peer.online = false;
+ });
+
+ let mut response_set = HashSet::new();
+ let mut last_write_time = Instant::now() - std::time::Duration::from_secs(4);
+ loop {
+ tokio::select! {
+ data = rx.recv() => match data {
+ Some(mut peer) => {
+ let in_response_set = !response_set.insert(peer.id.clone());
+ if let Some(pos) = peers.iter().position(|x| x.is_same_peer(&peer) ) {
+ let peer1 = peers.remove(pos);
+ if in_response_set {
+ peer.ip_mac.extend(peer1.ip_mac);
+ peer.online = true;
+ }
+ }
+ peers.insert(0, peer);
+ if last_write_time.elapsed().as_millis() > 300 {
+ config::LanPeers::store(&peers);
+ last_write_time = Instant::now();
+ }
+ }
+ None => {
+ break
+ }
+ }
+ }
+ }
+
+ config::LanPeers::store(&peers);
+ Ok(())
+}
diff --git a/src/lang.rs b/src/lang.rs
index 4b4998fc1..ec0f3c187 100644
--- a/src/lang.rs
+++ b/src/lang.rs
@@ -1,20 +1,48 @@
+use serde_json::{json, value::Value};
use std::ops::Deref;
mod cn;
mod cs;
mod da;
-mod sk;
mod de;
mod en;
-mod es;
mod eo;
+mod es;
+mod hu;
mod fr;
mod id;
mod it;
mod ptbr;
mod ru;
+mod sk;
mod tr;
mod tw;
+mod vn;
+mod pl;
+
+lazy_static::lazy_static! {
+ pub static ref LANGS: Value =
+ json!(vec![
+ ("en", "English"),
+ ("it", "Italiano"),
+ ("fr", "Français"),
+ ("de", "Deutsch"),
+ ("cn", "简体中文"),
+ ("tw", "繁體中文"),
+ ("pt", "Português"),
+ ("es", "Español"),
+ ("hu", "Magyar"),
+ ("ru", "Русский"),
+ ("sk", "Slovenčina"),
+ ("id", "Indonesia"),
+ ("cs", "Čeština"),
+ ("da", "Dansk"),
+ ("eo", "Esperanto"),
+ ("tr", "Türkçe"),
+ ("vn", "Tiếng Việt"),
+ ("pl", "Polski"),
+ ]);
+}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn translate(name: String) -> String {
@@ -46,6 +74,7 @@ pub fn translate_locale(name: String, locale: &str) -> String {
"tw" => tw::T.deref(),
"de" => de::T.deref(),
"es" => es::T.deref(),
+ "hu" => hu::T.deref(),
"ru" => ru::T.deref(),
"eo" => eo::T.deref(),
"id" => id::T.deref(),
@@ -56,6 +85,8 @@ pub fn translate_locale(name: String, locale: &str) -> String {
"cs" => cs::T.deref(),
"da" => da::T.deref(),
"sk" => sk::T.deref(),
+ "vn" => vn::T.deref(),
+ "pl" => pl::T.deref(),
_ => en::T.deref(),
};
if let Some(v) = m.get(&name as &str) {
diff --git a/src/lang/cn.rs b/src/lang/cn.rs
index a09ab8d39..bc06828a5 100644
--- a/src/lang/cn.rs
+++ b/src/lang/cn.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "关于"),
("Mute", "静音"),
("Audio Input", "音频输入"),
+ ("Enhancements", "增强功能"),
+ ("Hardware Codec", "硬件编解码"),
+ ("Adaptive Bitrate", "自适应码率"),
("ID Server", "ID服务器"),
("Relay Server", "中继服务器"),
("API Server", "API服务器"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "优化反应时间"),
("Custom", "自定义画质"),
("Show remote cursor", "显示远程光标"),
+ ("Show quality monitor", "显示质量监测"),
("Disable clipboard", "禁止剪贴板"),
("Lock after session end", "断开后锁定远程电脑"),
("Insert", "插入"),
@@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "退出"),
("In privacy mode", "进入隐私模式"),
("Out privacy mode", "退出隐私模式"),
+ ("Language", "语言"),
+ ("Keep RustDesk background service", "保持RustDesk后台服务"),
+ ("Ignore Battery Optimizations", "忽略电池优化"),
+ ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的RustDesk应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"),
+ ("Connection not allowed", "对方不允许连接"),
+ ("Use temporary password", "使用临时密码"),
+ ("Use permanent password", "使用固定密码"),
+ ("Use both passwords", "同时使用两种密码"),
+ ("Set permanent password", "设置固定密码"),
+ ("Set temporary password length", "设置临时密码长度"),
+ ("Enable Remote Restart", "允许远程重启"),
+ ("Allow remote restart", "允许远程重启"),
+ ("Restart Remote Device", "重启远程电脑"),
+ ("Are you sure you want to restart", "确定要重启"),
+ ("Restarting Remote Device", "正在重启远程设备"),
+ ("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"),
].iter().cloned().collect();
}
diff --git a/src/lang/cs.rs b/src/lang/cs.rs
index 80e1568eb..91437b2af 100644
--- a/src/lang/cs.rs
+++ b/src/lang/cs.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "O aplikaci"),
("Mute", "Ztlumit"),
("Audio Input", "Vstup zvuku"),
+ ("Enhancements", ""),
+ ("Hardware Codec", ""),
+ ("Adaptive Bitrate", ""),
("ID Server", "Server pro identif."),
("Relay Server", "Předávací (relay) server"),
("API Server", "Server s API rozhraním"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimalizovat pro co nejnižší prodlevu odezvy"),
("Custom", "Uživatelsky určené"),
("Show remote cursor", "Zobrazovat ukazatel myši z protějšku"),
+ ("Show quality monitor", ""),
("Disable clipboard", "Vypnout schránku"),
("Lock after session end", "Po ukončení relace zamknout plochu"),
("Insert", "Vložit"),
@@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Vypnutý"),
("In privacy mode", "v režimu soukromí"),
("Out privacy mode", "mimo režim soukromí"),
+ ("Language", ""),
+ ("Keep RustDesk background service", ""),
+ ("Ignore Battery Optimizations", ""),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", ""),
+ ("Use temporary password", ""),
+ ("Use permanent password", ""),
+ ("Use both passwords", ""),
+ ("Set permanent password", ""),
+ ("Set temporary password length", ""),
+ ("Enable Remote Restart", ""),
+ ("Allow remote restart", ""),
+ ("Restart Remote Device", ""),
+ ("Are you sure you want to restart", ""),
+ ("Restarting Remote Device", ""),
+ ("remote_restarting_tip", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/da.rs b/src/lang/da.rs
index 429b08b05..87e687936 100644
--- a/src/lang/da.rs
+++ b/src/lang/da.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "Omkring"),
("Mute", "Sluk for mikrofonen"),
("Audio Input", "Lydindgang"),
+ ("Enhancements", ""),
+ ("Hardware Codec", ""),
+ ("Adaptive Bitrate", ""),
("ID Server", "identifikations Server"),
("Relay Server", "Relæ Server"),
("API Server", "API Server"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimeret responstid"),
("Custom", "Brugerdefineret"),
("Show remote cursor", "Vis fjernbetjeningskontrolleret markør"),
+ ("Show quality monitor", ""),
("Disable clipboard", "Deaktiver udklipsholder"),
("Lock after session end", "Lås efter afslutningen af fjernstyring"),
("Insert", "Indsæt"),
@@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Slukket"),
("In privacy mode", "I databeskyttelsestilstand"),
("Out privacy mode", "Databeskyttelsestilstand fra"),
+ ("Language", ""),
+ ("Keep RustDesk background service", ""),
+ ("Ignore Battery Optimizations", ""),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", ""),
+ ("Use temporary password", ""),
+ ("Use permanent password", ""),
+ ("Use both passwords", ""),
+ ("Set permanent password", ""),
+ ("Set temporary password length", ""),
+ ("Enable Remote Restart", ""),
+ ("Allow remote restart", ""),
+ ("Restart Remote Device", ""),
+ ("Are you sure you want to restart", ""),
+ ("Restarting Remote Device", ""),
+ ("remote_restarting_tip", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/de.rs b/src/lang/de.rs
index 8bbfbb4c4..8acc30991 100644
--- a/src/lang/de.rs
+++ b/src/lang/de.rs
@@ -6,14 +6,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("desk_tip", "Mit dieser ID und diesem Passwort können Sie auf Ihren Desktop zugreifen."),
("Password", "Passwort"),
("Ready", "Bereit"),
- ("Established", "Etabliert"),
+ ("Established", "Verbunden"),
("connecting_status", "Verbinden mit dem RustDesk-Netzwerk..."),
- ("Enable Service", "Verbindungsserver einschalten"),
- ("Start Service", "Starte Verbindungsserver"),
- ("Service is running", "Dienst läuft"),
- ("Service is not running", "Der Verbindungsserver läuft nicht"),
+ ("Enable Service", "Verbindungsserver aktivieren"),
+ ("Start Service", "Starte Vermittlungsdienst"),
+ ("Service is running", "Vermittlungsdienst aktiv"),
+ ("Service is not running", "Vermittlungsdienst deaktiviert"),
("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Verbindung"),
- ("Control Remote Desktop", "Entfernten Desktop steuern"),
+ ("Control Remote Desktop", "Entfernten PC steuern"),
("Transfer File", "Datei übertragen"),
("Connect", "Verbinden"),
("Recent Sessions", "Letzte Sitzungen"),
@@ -21,46 +21,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Confirmation", "Bestätigung"),
("TCP Tunneling", "TCP Tunneln"),
("Remove", "Entfernen"),
- ("Refresh random password", "Zufälliges Passwort aktualisieren"),
- ("Set your own password", "Legen Sie Ihr eigenes Passwort fest"),
- ("Enable Keyboard/Mouse", "Tastatur/Maus einschalten"),
- ("Enable Clipboard", "Zwischenablage einschalten"),
+ ("Refresh random password", "Neues zufälliges Passwort"),
+ ("Set your own password", "Eigenes Passwort setzen"),
+ ("Enable Keyboard/Mouse", "Tastatur/Maus aktivieren"),
+ ("Enable Clipboard", "Zwischenablage aktivieren"),
("Enable File Transfer", "Dateiübertragung aktivieren"),
- ("Enable TCP Tunneling", "TCP-Tunneling einschalten"),
- ("IP Whitelisting", "IP Freigabeliste"),
- ("ID/Relay Server", "ID/Verbindungsserver"),
- ("Stop service", "Verbindungsserver ausschalten"),
- ("Change ID", "ID wechseln"),
+ ("Enable TCP Tunneling", "TCP-Tunneln aktivieren"),
+ ("IP Whitelisting", "IP-Whitelist"),
+ ("ID/Relay Server", "ID/Vermittlungsserver"),
+ ("Stop service", "Vermittlungsdienst deaktivieren"),
+ ("Change ID", "ID ändern"),
("Website", "Webseite"),
("About", "Über"),
("Mute", "Stummschalten"),
("Audio Input", "Audio-Eingang"),
+ ("Enhancements", "Verbesserungen"),
+ ("Hardware Codec", "Hardware-Codec"),
+ ("Adaptive Bitrate", "Adaptive Bitrate"),
("ID Server", "ID Server"),
- ("Relay Server", "Verbindungsserver Server"),
- ("API Server", "API Server"),
+ ("Relay Server", "Vermittlungsserver"),
+ ("API Server", "API-Server"),
("invalid_http", "Muss mit http:// oder https:// beginnen"),
("Invalid IP", "Ungültige IP-Adresse"),
- ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein. Länge zwischen 6 und 16."),
+ ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein, Länge zwischen 6 und 16."),
("Invalid format", "Ungültiges Format"),
- ("server_not_support", "Noch nicht vom Server unterstützt"),
+ ("server_not_support", "Diese Funktion wird noch nicht vom Server unterstützt"),
("Not available", "Nicht verfügbar"),
("Too frequent", "Zu häufig"),
("Cancel", "Abbrechen"),
("Skip", "Überspringen"),
("Close", "Schließen"),
- ("Retry", "Nochmal versuchen"),
+ ("Retry", "Erneut versuchen"),
("OK", "OK"),
("Password Required", "Passwort erforderlich"),
- ("Please enter your password", "Bitte geben Sie Ihr Passwort ein"),
+ ("Please enter your password", "Bitte geben Sie das Passwort des entfernten PCs ein."),
("Remember password", "Passwort merken"),
("Wrong Password", "Falsches Passwort"),
- ("Do you want to enter again?", "Möchten Sie erneut teilnehmen?"),
+ ("Do you want to enter again?", "Erneut verbinden?"),
("Connection Error", "Verbindungsfehler"),
("Error", "Fehler"),
- ("Reset by the peer", "Zurücksetzen durch die Gegenstelle"),
+ ("Reset by the peer", "Verbindung wurde von der Gegenstelle zurückgesetzt"),
("Connecting...", "Verbinden..."),
("Connection in progress. Please wait.", "Die Verbindung wird hergestellt. Bitte warten Sie."),
- ("Please try 1 minute later", "Bitte versuchen Sie es 1 Minute später"),
+ ("Please try 1 minute later", "Bitte versuchen Sie es später erneut"),
("Login Error", "Anmeldefehler"),
("Successful", "Erfolgreich"),
("Connected, waiting for image...", "Verbunden, warten auf Bild..."),
@@ -72,91 +75,92 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Receive", "Empfangen"),
("Send", "Senden"),
("Refresh File", "Datei aktualisieren"),
- ("Local", "Lokaler"),
- ("Remote", "Entfernter"),
+ ("Local", "Lokal"),
+ ("Remote", "Entfernt"),
("Remote Computer", "Entfernter Computer"),
- ("Local Computer", "Lokaler Computer"),
+ ("Local Computer", "Dieser Computer"),
("Confirm Delete", "Löschen bestätigen"),
("Delete", "Löschen"),
("Properties", "Eigenschaften"),
("Multi Select", "Mehrfachauswahl"),
- ("Empty Directory", "Leeres Verzeichnis"),
- ("Not an empty directory", "Kein leeres Verzeichnis"),
+ ("Empty Directory", "Leerer Ordner"),
+ ("Not an empty directory", "Ordner nicht leer"),
("Are you sure you want to delete this file?", "Sind Sie sicher, dass Sie diese Datei löschen wollen?"),
- ("Are you sure you want to delete this empty directory?", "Sind Sie sicher, dass Sie dieses leere Verzeichnis löschen möchten?"),
- ("Are you sure you want to delete the file of this directory?", "Sind Sie sicher, dass Sie die Datei dieses Verzeichnisses löschen möchten?"),
- ("Do this for all conflicts", "Dies gilt für alle Konflikte"),
+ ("Are you sure you want to delete this empty directory?", "Sind Sie sicher, dass Sie diesen leeren Ordner löschen möchten?"),
+ ("Are you sure you want to delete the file of this directory?", "Sind Sie sicher, dass Sie die Datei dieses Ordners löschen möchten?"),
+ ("Do this for all conflicts", "Für alle Konflikte merken"),
("This is irreversible!", "Dies ist irreversibel!"),
("Deleting", "Löschen"),
("files", "Dateien"),
("Waiting", "Warten"),
("Finished", "Fertiggestellt"),
("Speed", "Geschwindigkeit"),
- ("Custom Image Quality", "Individuelle Bildqualität"),
+ ("Custom Image Quality", "Benutzerdefinierte Bildqualität"),
("Privacy mode", "Datenschutz-Modus"),
("Block user input", "Benutzereingaben blockieren"),
("Unblock user input", "Benutzereingaben freigeben"),
("Adjust Window", "Fenster anpassen"),
("Original", "Original"),
- ("Shrink", "Geschrumpft"),
- ("Stretch", "Gestreckt"),
- ("Good image quality", "Gute Bildqualität"),
+ ("Shrink", "Verkleinern"),
+ ("Stretch", "Strecken"),
+ ("Good image quality", "Schöner"),
("Balanced", "Ausgeglichen"),
- ("Optimize reaction time", "Optimierte Reaktionszeit"),
+ ("Optimize reaction time", "Schneller"),
("Custom", "Benutzerdefiniert"),
- ("Show remote cursor", "Ferngesteuerten Cursor anzeigen"),
+ ("Show remote cursor", "Entfernten Cursor anzeigen"),
+ ("Show quality monitor", "Qualitätsüberwachung anzeigen"),
("Disable clipboard", "Zwischenablage deaktivieren"),
("Lock after session end", "Sperren nach Sitzungsende"),
("Insert", "Einfügen"),
- ("Insert Lock", "Sperre einfügen"),
+ ("Insert Lock", "Win+L (Sperren) senden"),
("Refresh", "Aktualisieren"),
- ("ID does not exist", "Die ID existiert nicht"),
- ("Failed to connect to rendezvous server", "Verbindung zum Verbindungsserver fehlgeschlagen"),
- ("Please try later", "Bitte versuchen Sie es später"),
- ("Remote desktop is offline", "Entfernter Desktop ist offline"),
- ("Key mismatch", "Schlüssel nicht übereinstimmend"),
+ ("ID does not exist", "Diese ID existiert nicht"),
+ ("Failed to connect to rendezvous server", "Verbindung zum Vermittlungsserver fehlgeschlagen"),
+ ("Please try later", "Bitte versuchen Sie es später erneut"),
+ ("Remote desktop is offline", "Entfernter PC ist offline"),
+ ("Key mismatch", "Schlüssel stimmt nicht mit Serverschlüssel überein"),
("Timeout", "Zeitüberschreitung"),
- ("Failed to connect to relay server", "Verbindung zum Verbindungsserver fehlgeschlagen"),
- ("Failed to connect via rendezvous server", "Verbindung über rendezvous server fehlgeschlagen"),
- ("Failed to connect via relay server", "Verbindung über den Verbindungsserver ist fehlgeschlagen"),
- ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum Entfernten-Desktop konnte nicht hergestellt werden"),
+ ("Failed to connect to relay server", "Verbindung zum Vermittlungsserver fehlgeschlagen"),
+ ("Failed to connect via rendezvous server", "Verbindung über Vermittlungsserver ist fehlgeschlagen"),
+ ("Failed to connect via relay server", "Verbindung über Relay-Server ist fehlgeschlagen"),
+ ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten PC fehlgeschlagen"),
("Set Password", "Passwort festlegen"),
("OS Password", "Betriebssystem-Passwort"),
("install_tip", "Aufgrund der UAC kann RustDesk in manchen Fällen nicht ordnungsgemäß auf der Gegenseite funktionieren. Um UAC zu vermeiden, klicken Sie bitte auf die Schaltfläche unten, um RustDesk auf dem System zu installieren"),
- ("Click to upgrade", "Zum Upgrade anklicken"),
+ ("Click to upgrade", "Zum Aktualisieren anklicken"),
("Click to download", "Zum Herunterladen klicken"),
("Click to update", "Zum Aktualisieren klicken"),
("Configure", "Konfigurieren"),
- ("config_acc", "Um Ihren Desktop aus der Ferne zu steuern, müssen Sie RustDesk \"Zugangs\" Rechte erteilen."),
- ("config_screen", "Um aus der Ferne auf Ihren Desktop zugreifen zu können, müssen Sie RustDesk \"Bildschirm-Aufnahme\" Berechtigungen erteilen."),
+ ("config_acc", "Um Ihren PC aus der Ferne zu steuern, müssen Sie RustDesk Zugriffsrechte erteilen."),
+ ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk \"Bildschirm-Aufnahme\"-Berechtigung erteilen."),
("Installing ...", "Installiere ..."),
("Install", "Installieren"),
- ("Installation", "Einrichtung"),
- ("Installation Path", "Einrichtungs Pfad"),
- ("Create start menu shortcuts", "Startmenü Verknüpfungen erstellen"),
- ("Create desktop icon", "Desktop Symbol erstellen"),
- ("agreement_tip", "Wenn Sie die Einrichtung starten, akzeptieren Sie die Lizenzvereinbarung"),
- ("Accept and Install", "Akzeptieren und installieren"),
+ ("Installation", "Installation"),
+ ("Installation Path", "Installationspfad"),
+ ("Create start menu shortcuts", "Verknüpfung im Startmenü erstellen"),
+ ("Create desktop icon", "Desktop-Verknüpfung erstellen"),
+ ("agreement_tip", "Durch die Installation akzeptieren Sie die Lizenzvereinbarung"),
+ ("Accept and Install", "Akzeptieren und Installieren"),
("End-user license agreement", "Lizenzvereinbarung für Endbenutzer"),
- ("Generating ...", "Generierung ..."),
- ("Your installation is lower version.", "Ihre Installation ist eine niedrigere Version."),
- ("not_close_tcp_tip", "Schließen Sie dieses Fenster nicht, während Sie den Tunnel benutzen."),
- ("Listening ...", "Hören ..."),
- ("Remote Host", "Entfernter Rechner"),
+ ("Generating ...", "Generiere..."),
+ ("Your installation is lower version.", "Ihre Installation ist älter."),
+ ("not_close_tcp_tip", "Schließen Sie dieses Fenster nicht, solange Sie den Tunnel benutzen."),
+ ("Listening ...", "Höre..."),
+ ("Remote Host", "Entfernter PC"),
("Remote Port", "Entfernter Port"),
("Action", "Aktion"),
("Add", "Hinzufügen"),
("Local Port", "Lokaler Port"),
- ("setup_server_tip", "Für eine schnellere Verbindung, richten Sie bitte Ihren eigenen Verbindungsserver ein"),
+ ("setup_server_tip", "Für eine schnellere Verbindung richten Sie bitte Ihren eigenen Verbindungsserver ein"),
("Too short, at least 6 characters.", "Zu kurz, mindestens 6 Zeichen."),
- ("The confirmation is not identical.", "Die Bestätigung ist nicht identisch."),
+ ("The confirmation is not identical.", "Die Passwörter sind nicht identisch."),
("Permissions", "Berechtigungen"),
("Accept", "Akzeptieren"),
("Dismiss", "Ablehnen"),
("Disconnect", "Verbindung trennen"),
- ("Allow using keyboard and mouse", "Erlaubt die Verwendung von Tastatur und Maus"),
+ ("Allow using keyboard and mouse", "Verwendung von Maus und Tastatur zulassen"),
("Allow using clipboard", "Verwendung der Zwischenablage zulassen"),
- ("Allow hearing sound", "Erlaubt das Hören von Sound"),
+ ("Allow hearing sound", "System-Audio übertragen"),
("Allow file copy and paste", "Kopieren und Einfügen von Dateien zulassen"),
("Connected", "Verbunden"),
("Direct and encrypted connection", "Direkte und verschlüsselte Verbindung"),
@@ -167,17 +171,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enter your password", "Geben Sie Ihr Passwort ein"),
("Logging in...", "Anmeldung..."),
("Enable RDP session sharing", "RDP-Sitzungsfreigabe aktivieren"),
- ("Auto Login", "Automatisches Login (nur gültig, wenn Sie \"Sperren nach Sitzungsende\" eingestellt haben)"),
+ ("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Sperren nach Sitzungsende\" aktiviert haben)"),
("Enable Direct IP Access", "Direkten IP-Zugang aktivieren"),
("Rename", "Umbenennen"),
- ("Space", "Platz"),
+ ("Space", "Speicherplatz"),
("Create Desktop Shortcut", "Desktop-Verknüpfung erstellen"),
("Change Path", "Pfad ändern"),
("Create Folder", "Ordner erstellen"),
("Please enter the folder name", "Bitte geben Sie den Ordnernamen ein"),
("Fix it", "Reparieren"),
("Warning", "Warnung"),
- ("Login screen using Wayland is not supported", "Anmeldebildschirm mit Wayland wird nicht unterstützt"),
+ ("Login screen using Wayland is not supported", "Anmeldebildschirm wird mit Wayland nicht unterstützt"),
("Reboot required", "Neustart erforderlich"),
("Unsupported display server ", "Nicht unterstützter Display-Server"),
("x11 expected", "X11 erwartet"),
@@ -185,14 +189,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Settings", "Einstellungen"),
("Username", " Benutzername"),
("Invalid port", "Ungültiger Port"),
- ("Closed manually by the peer", "Vom Peer manuell geschlossen"),
- ("Enable remote configuration modification", "Änderung der Fernkonfiguration zulassen"),
+ ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"),
+ ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"),
("Run without install", "Ohne Installation ausführen"),
- ("Always connected via relay", "Immer über Verbindungsserver verbunden"),
- ("Always connect via relay", "Verbindung immer über Verbindungsserver"),
- ("whitelist_tip", "Nur IPs auf der Freigabeliste können auf mich zugreifen"),
- ("Login", "Anmeldung"),
- ("Logout", "Abmeldung"),
+ ("Always connected via relay", "Immer über Relay-Server verbunden"),
+ ("Always connect via relay", "Immer über Relay-Server verbinden"),
+ ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen"),
+ ("Login", "Anmelden"),
+ ("Logout", "Abmelden"),
("Tags", "Stichworte"),
("Search ID", "Suche ID"),
("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt"),
@@ -201,83 +205,99 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Add Tag", "Stichwort hinzufügen"),
("Unselect all tags", "Alle Stichworte abwählen"),
("Network error", "Netzwerkfehler"),
- ("Username missed", "Benutzername fehlt"),
+ ("Username missed", "Benutzername vergessen"),
("Password missed", "Passwort vergessen"),
("Wrong credentials", "Falsche Anmeldedaten"),
("Edit Tag", "Stichwort bearbeiten"),
- ("Unremember Password", "Passwort nicht merken"),
+ ("Unremember Password", "Passwort vergessen"),
("Favorites", "Favoriten"),
("Add to Favorites", "Zu Favoriten hinzufügen"),
- ("Remove from Favorites", "Entferne von Favoriten"),
+ ("Remove from Favorites", "Aus Favoriten entfernen"),
("Empty", "Leer"),
("Invalid folder name", "Ungültiger Ordnername"),
("Socks5 Proxy", "Socks5 Proxy"),
("Hostname", "Rechnername"),
("Discovered", "Gefunden"),
- ("install_daemon_tip", "Um beim Booten zu starten, müssen Sie den Systemdienst installieren"),
+ ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein"),
("Remote ID", "Entfernte ID"),
("Paste", "Einfügen"),
("Paste here?", "Hier einfügen?"),
- ("Are you sure to close the connection?", "Sind Sie sicher, dass Sie die Verbindung schließen wollen?"),
+ ("Are you sure to close the connection?", "Möchten Sie diese Verbindung wirklich trennen?"),
("Download new version", "Neue Version herunterladen"),
("Touch mode", "Touch-Modus"),
- ("Mouse mode", "Mouse-Modus"),
- ("One-Finger Tap", "Ein Fingertipp"),
- ("Left Mouse", "Linke Maus"),
- ("One-Long Tap", "Tippen Sie mit einem Finger lang"),
- ("Two-Finger Tap", "Zwei Finger tippen"),
- ("Right Mouse", "Rechte Maus"),
- ("One-Finger Move", "Eine Fingerbewegung"),
- ("Double Tap & Move", "Doppeltippen und verschieben"),
- ("Mouse Drag", "Maus ziehen"),
- ("Three-Finger vertically", "Drei Finger vertikal"),
+ ("Mouse mode", "Maus-Modus"),
+ ("One-Finger Tap", "1-Finger-Tipp"),
+ ("Left Mouse", "Linksklick"),
+ ("One-Long Tap", "1-Finger-Halten"),
+ ("Two-Finger Tap", "2-Finger-Tipp"),
+ ("Right Mouse", "Rechtsklick"),
+ ("One-Finger Move", "Einen Finger bewegen"),
+ ("Double Tap & Move", "Doppeltippen und bewegen"),
+ ("Mouse Drag", "Maus bewegen"),
+ ("Three-Finger vertically", "Drei Finger vertikal bewegen"),
("Mouse Wheel", "Mausrad"),
- ("Two-Finger Move", "Zwei Finger Bewegung"),
- ("Canvas Move", "Leinwand bewegen"),
- ("Pinch to Zoom", "Zum Zoomen kneifen"),
- ("Canvas Zoom", "Leinwand Zoom"),
- ("Reset canvas", "Anzeige zurücksetzen"),
- ("No permission of file transfer", "Keine Erlaubnis zur Dateiübertragung"),
- ("Note", "Notiz"),
+ ("Two-Finger Move", "Zwei Finger bewegen"),
+ ("Canvas Move", "Sichtfeld bewegen"),
+ ("Pinch to Zoom", "2-Finger-Zoom"),
+ ("Canvas Zoom", "Sichtfeld-Zoom"),
+ ("Reset canvas", "Sichtfeld zurücksetzen"),
+ ("No permission of file transfer", "Keine Dateizugriff-Berechtigung"),
+ ("Note", "Anmerkung"),
("Connection", "Verbindung"),
("Share Screen", "Bildschirm freigeben"),
- ("CLOSE", "NAH DRAN"),
- ("OPEN", "OFFEN"),
- ("Chat", "Plaudern"),
+ ("CLOSE", "DEAKTIV."),
+ ("OPEN", "AKTIVIER."),
+ ("Chat", "Chat"),
("Total", "Gesamt"),
- ("items", "Artikel"),
+ ("items", "Einträge"),
("Selected", "Ausgewählt"),
- ("Screen Capture", "Bildschirmaufnahme"),
- ("Input Control", "Eingabesteuerung"),
- ("Audio Capture", "Audioaufnahme"),
- ("File Connection", "Dateiverbindung"),
+ ("Screen Capture", "Bildschirmzugr."),
+ ("Input Control", "Eingabezugriff"),
+ ("Audio Capture", "Audiozugriff"),
+ ("File Connection", "Dateizugriff"),
("Screen Connection", "Bildschirmanschluss"),
- ("Do you accept?", "Akzeptieren Sie?"),
+ ("Do you accept?", "Verbindung zulassen?"),
("Open System Setting", "Systemeinstellung öffnen"),
("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"),
- ("android_input_permission_tip1", "Damit ein Remote-Gerät Ihr Android-Gerät per Maus oder Berührung steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."),
+ ("android_input_permission_tip1", "Damit ein Remote-Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."),
("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen und geben Sie [Installierte Dienste] ein, schalten Sie den Dienst [RustDesk Input] ein."),
- ("android_new_connection_tip", "Es wurde eine neue Steuerungsanforderung empfangen, die Ihr aktuelles Gerät steuern möchte."),
- ("android_service_will_start_tip", "Durch das Einschalten der Bildschirmaufnahme wird der Dienst automatisch gestartet, sodass andere Geräte eine Verbindung von diesem Gerät anfordern können."),
- ("android_stop_service_tip", "Durch das Schließen des Dienstes werden automatisch alle hergestellten Verbindungen geschlossen."),
- ("android_version_audio_tip", "Die aktuelle Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher."),
- ("android_start_service_tip", "Tippen Sie auf [Dienst starten] oder ÖFFNEN Sie die Berechtigung [Bildschirmaufnahme], um den Bildschirmfreigabedienst zu starten."),
+ ("android_new_connection_tip", "möchte ihr Gerät steuern."),
+ ("android_service_will_start_tip", "Durch das Aktivieren der Bildschirmfreigabe wird der Dienst automatisch gestartet, sodass andere Geräte dieses Android-Gerät steuern können."),
+ ("android_stop_service_tip", "Durch das Deaktivieren des Dienstes werden automatisch alle hergestellten Verbindungen getrennt."),
+ ("android_version_audio_tip", "Ihre Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher, falls möglich."),
+ ("android_start_service_tip", "Tippen Sie auf [Dienst aktivieren] oder aktivieren Sie die Berechtigung [Bildschirmzugr.], um den Bildschirmfreigabedienst zu starten."),
("Account", "Konto"),
("Overwrite", "Überschreiben"),
- ("This file exists, skip or overwrite this file?", "Diese Datei existiert, diese Datei überspringen oder überschreiben?"),
- ("Quit", "Aufhören"),
- ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"),
+ ("This file exists, skip or overwrite this file?", "Diese Datei existiert; überspringen oder überschreiben?"),
+ ("Quit", "Beenden"),
+ ("doc_mac_permission", "https://rustdesk.com/docs/de/manual/mac/#berechtigungen-aktivieren"),
("Help", "Hilfe"),
- ("Failed", "Gescheitert"),
+ ("Failed", "Fehlgeschlagen"),
("Succeeded", "Erfolgreich"),
- ("Someone turns on privacy mode, exit", "Jemand aktiviert den Datenschutzmodus, beenden"),
+ ("Someone turns on privacy mode, exit", "Jemand hat den Datenschutzmodus aktiviert, beende..."),
("Unsupported", "Nicht unterstützt"),
- ("Peer denied", "Peer verweigert"),
+ ("Peer denied", "Die Gegenstelle hat die Verbindung abgelehnt"),
("Please install plugins", "Bitte installieren Sie Plugins"),
- ("Peer exit", "Peer-Ausgang"),
+ ("Peer exit", "Die Gegenstelle hat die Verbindung getrennt"),
("Failed to turn off", "Ausschalten fehlgeschlagen"),
("Turned off", "Ausgeschaltet"),
- ("In privacy mode", "im Datenschutzmodus"),
- ("Out privacy mode", "Datenschutzmodus aus"),
+ ("In privacy mode", "Datenschutzmodus aktivieren"),
+ ("Out privacy mode", "Datenschutzmodus deaktivieren"),
+ ("Language", "Sprache"),
+ ("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"),
+ ("Ignore Battery Optimizations", "Batterieoptimierung ignorieren"),
+ ("android_open_battery_optimizations_tip", "Möchten Sie die Batterieopimierungs-Einstellungen öffnen?"),
+ ("Connection not allowed", "Verbindung abgelehnt"),
+ ("Use temporary password", "Temporäres Passwort verwenden"),
+ ("Use permanent password", "Dauerhaftes Passwort verwenden"),
+ ("Use both passwords", "Beide Passwörter verwenden"),
+ ("Set permanent password", "Dauerhaftes Passwort setzen"),
+ ("Set temporary password length", "Länge des temporären Passworts setzen"),
+ ("Enable Remote Restart", "Entfernten Neustart aktivieren"),
+ ("Allow remote restart", "Entfernten Neustart erlauben"),
+ ("Restart Remote Device", "Entferntes Gerät neu starten"),
+ ("Are you sure you want to restart", "Möchten Sie das entfernte Gerät wirklich neu starten?"),
+ ("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"),
+ ("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."),
].iter().cloned().collect();
}
diff --git a/src/lang/en.rs b/src/lang/en.rs
index a4d11d415..8b21af4ef 100644
--- a/src/lang/en.rs
+++ b/src/lang/en.rs
@@ -27,5 +27,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"),
("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"),
("server_not_support", "Not yet supported by the server"),
+ ("android_open_battery_optimizations_tip", "If you want to disable this feature, please go to the next RustDesk application settings page, find and enter [Battery], Uncheck [Unrestricted]"),
+ ("remote_restarting_tip", "Remote device is restarting, please close this message box and reconnect with permanent password after a while"),
].iter().cloned().collect();
}
diff --git a/src/lang/eo.rs b/src/lang/eo.rs
index 49c9f38fa..1be29dd07 100644
--- a/src/lang/eo.rs
+++ b/src/lang/eo.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "Pri"),
("Mute", "Muta"),
("Audio Input", "Aŭdia enigo"),
+ ("Enhancements", ""),
+ ("Hardware Codec", ""),
+ ("Adaptive Bitrate", ""),
("ID Server", "Servilo de identigiloj"),
("Relay Server", "Relajsa servilo"),
("API Server", "Servilo de API"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimigi reakcia tempo"),
("Custom", "Personigi bilda kvalito"),
("Show remote cursor", "Montri foran kursoron"),
+ ("Show quality monitor", ""),
("Disable clipboard", "Malebligi poŝon"),
("Lock after session end", "Ŝlosi foran komputilon post malkonektado"),
("Insert", "Enmeti"),
@@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", ""),
("In privacy mode", ""),
("Out privacy mode", ""),
+ ("Language", ""),
+ ("Keep RustDesk background service", ""),
+ ("Ignore Battery Optimizations", ""),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", ""),
+ ("Use temporary password", ""),
+ ("Use permanent password", ""),
+ ("Use both passwords", ""),
+ ("Set permanent password", ""),
+ ("Set temporary password length", ""),
+ ("Enable Remote Restart", ""),
+ ("Allow remote restart", ""),
+ ("Restart Remote Device", ""),
+ ("Are you sure you want to restart", ""),
+ ("Restarting Remote Device", ""),
+ ("remote_restarting_tip", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/es.rs b/src/lang/es.rs
index ba4c671a8..8ae2feecd 100644
--- a/src/lang/es.rs
+++ b/src/lang/es.rs
@@ -3,15 +3,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
("Status", "Estado"),
("Your Desktop", "Tu escritorio"),
- ("desk_tip", "Puoi accedere al tuo desktop usando l'ID e la password riportati qui."),
+ ("desk_tip", "Tu escritorio puede ser ingresado con esta ID y contraseña."),
("Password", "Contraseña"),
("Ready", "Listo"),
("Established", "Establecido"),
("connecting_status", "Conexión a la red RustDesk en progreso..."),
("Enable Service", "Habilitar Servicio"),
("Start Service", "Iniciar Servicio"),
- ("Service is running", "Servicio se está ejecutando"),
- ("Service is not running", "Servicio no se está ejecutando"),
+ ("Service is running", "El servicio se está ejecutando"),
+ ("Service is not running", "El servicio no se está ejecutando"),
("not_ready_status", "No está listo. Comprueba tu conexión"),
("Control Remote Desktop", "Controlar Escritorio Remoto"),
("Transfer File", "Transferir archivo"),
@@ -27,14 +27,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable Clipboard", "Habilitar portapapeles"),
("Enable File Transfer", "Habilitar transferencia de archivos"),
("Enable TCP Tunneling", "Habilitar tunel TCP"),
- ("IP Whitelisting", "Lista blanca IP"),
- ("ID/Relay Server", "Servidor de ID/Relay"),
+ ("IP Whitelisting", "Lista blanca de IP"),
+ ("ID/Relay Server", "Servidor ID/Relay"),
("Stop service", "Parar servicio"),
- ("Change ID", "Cambiar identificación"),
+ ("Change ID", "Cambiar ID"),
("Website", "Sitio web"),
- ("About", "Sobre"),
+ ("About", "Acerca de"),
("Mute", "Silencio"),
("Audio Input", "Entrada de audio"),
+ ("Enhancements", "mejoras"),
+ ("Hardware Codec", ""),
+ ("Adaptive Bitrate", ""),
("ID Server", "ID server"),
("Relay Server", "Server relay"),
("API Server", "Server API"),
@@ -43,12 +46,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9 e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 a 16 caracteres."),
("Invalid format", "Formato inválido"),
("server_not_support", "Aún no es compatible con el servidor"),
- ("Not available", "Indisponible"),
+ ("Not available", "No disponible"),
("Too frequent", "Demasiado frecuente"),
("Cancel", "Cancelar"),
("Skip", "Saltar"),
("Close", "Cerrar"),
- ("Retry", "Volver"),
+ ("Retry", "Reintentar"),
("OK", "OK"),
("Password Required", "Se requiere contraseña"),
("Please enter your password", "Por favor, introduzca su contraseña"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimizar el tiempo de reacción"),
("Custom", "Personalizado"),
("Show remote cursor", "Mostrar cursor remoto"),
+ ("Show quality monitor", "Mostrar calidad del monitor"),
("Disable clipboard", "Deshabilitar portapapeles"),
("Lock after session end", "Bloquear después del final de la sesión"),
("Insert", "Insertar"),
@@ -113,7 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ID does not exist", "ID no existe"),
("Failed to connect to rendezvous server", "No se pudo conectar al servidor de encuentro"),
("Please try later", "Por favor intente mas tarde"),
- ("Remote desktop is offline", "El escritorio remoto está fuera de línea"),
+ ("Remote desktop is offline", "El escritorio remoto está desconectado"),
("Key mismatch", "La clave no coincide"),
("Timeout", "Timeout"),
("Failed to connect to relay server", "No se pudo conectar al servidor de retransmisión"),
@@ -125,7 +129,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("install_tip", "Debido al Control de cuentas de usuario, es posible que RustDesk no funcione correctamente como escritorio remoto. Para evitar este problema, haga clic en el botón de abajo para instalar RustDesk a nivel de sistema."),
("Click to upgrade", "Clic para actualizar"),
("Click to download", "Clic para descargar"),
- ("Click to update", "Fare clic per aggiornare"),
+ ("Click to update", "Clic para refrescar"),
("Configure", "Configurar"),
("config_acc", "Para controlar su escritorio desde el exterior, debe otorgar permiso a RustDesk de \"Accesibilidad\"."),
("config_screen", "Para controlar su escritorio desde el exterior, debe otorgar permiso a RustDesk de \"Grabación de pantalla\"."),
@@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Apagado"),
("In privacy mode", "En modo de privacidad"),
("Out privacy mode", "Fuera del modo de privacidad"),
+ ("Language", "Idioma"),
+ ("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"),
+ ("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", "Conexión no disponible"),
+ ("Use temporary password", "Usar contraseña temporal"),
+ ("Use permanent password", "Usar contraseña permamente"),
+ ("Use both passwords", "Usar ambas comtraseñas"),
+ ("Set permanent password", "Establecer contraseña permamente"),
+ ("Set temporary password length", "Establecer largo de contraseña temporal"),
+ ("Enable Remote Restart", "Activar reinicio remoto"),
+ ("Allow remote restart", "Permitir reinicio remoto"),
+ ("Restart Remote Device", "Reiniciar dispositivo"),
+ ("Are you sure you want to restart", "Esta Seguro que desea reiniciar?"),
+ ("Restarting Remote Device", "Reiniciando dispositivo remoto"),
+ ("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."),
].iter().cloned().collect();
}
diff --git a/src/lang/fr.rs b/src/lang/fr.rs
index d2d41c550..7dd0fb9a9 100644
--- a/src/lang/fr.rs
+++ b/src/lang/fr.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "À propos de"),
("Mute", "Muet"),
("Audio Input", "Entrée audio"),
+ ("Enhancements", ""),
+ ("Hardware Codec", ""),
+ ("Adaptive Bitrate", ""),
("ID Server", "Serveur ID"),
("Relay Server", "Serveur relais"),
("API Server", "Serveur API"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimiser le temps de réaction"),
("Custom", "Qualité d'image personnalisée"),
("Show remote cursor", "Afficher le curseur distant"),
+ ("Show quality monitor", ""),
("Disable clipboard", "Désactiver le presse-papier"),
("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"),
("Insert", "Insérer"),
@@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Éteindre"),
("In privacy mode", "en mode privé"),
("Out privacy mode", "hors mode de confidentialité"),
+ ("Language", "Langue"),
+ ("Keep RustDesk background service", ""),
+ ("Ignore Battery Optimizations", ""),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", ""),
+ ("Use temporary password", ""),
+ ("Use permanent password", ""),
+ ("Use both passwords", ""),
+ ("Set permanent password", ""),
+ ("Set temporary password length", ""),
+ ("Enable Remote Restart", ""),
+ ("Allow remote restart", ""),
+ ("Restart Remote Device", ""),
+ ("Are you sure you want to restart", ""),
+ ("Restarting Remote Device", ""),
+ ("remote_restarting_tip", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/hu.rs b/src/lang/hu.rs
new file mode 100644
index 000000000..de9f8922b
--- /dev/null
+++ b/src/lang/hu.rs
@@ -0,0 +1,303 @@
+lazy_static::lazy_static! {
+pub static ref T: std::collections::HashMap<&'static str, &'static str> =
+ [
+ ("Status", "Státusz"),
+ ("Your Desktop", "A te asztalod"),
+ ("desk_tip", "Az asztalod ezzel az ID-vel, és jelszóval érhető el."),
+ ("Password", "Jelszó"),
+ ("Ready", "Kész"),
+ ("Established", "Létrejött"),
+ ("connecting_status", "Kapcsolódás a RustDesk hálózatához..."),
+ ("Enable Service", "A szolgáltatás bekapcsolása"),
+ ("Start Service", "Szolgáltatás Elindítása"),
+ ("Service is running", "A szolgáltatás fut"),
+ ("Service is not running", "A szolgáltatás nem fut"),
+ ("not_ready_status", "A RustDesk nem áll készen. Kérlek nézd meg a hálózati beállításaidat."),
+ ("Control Remote Desktop", "Távoli Asztal Kontrollálása"),
+ ("Transfer File", "Fájl Transzfer"),
+ ("Connect", "Kapcsolódás"),
+ ("Recent Sessions", "Korábbi Sessionök"),
+ ("Address Book", "Címköny"),
+ ("Confirmation", "Megerősít"),
+ ("TCP Tunneling", "TCP Tunneling"),
+ ("Remove", "Eltávolít"),
+ ("Refresh random password", "Véletlenszerű jelszó frissítése"),
+ ("Set your own password", "Saját jelszó beállítása"),
+ ("Enable Keyboard/Mouse", "Billentyűzet/Egér bekapcsolása"),
+ ("Enable Clipboard", "Megosztott vágólap bekapcsolása"),
+ ("Enable File Transfer", "Fájl transzer bekapcsolása"),
+ ("Enable TCP Tunneling", "TCP Tunneling bekapcsolása"),
+ ("IP Whitelisting", "IP Fehérlista"),
+ ("ID/Relay Server", "ID/Relay Szerver"),
+ ("Stop service", "Szolgáltatás Kikapcsolása"),
+ ("Change ID", "ID Megváltoztatása"),
+ ("Website", "Weboldal"),
+ ("About", "Rólunk: "),
+ ("Mute", "Némítás"),
+ ("Audio Input", "Audo Bemenet"),
+ ("Enhancements", "Javítások"),
+ ("Hardware Codec", "Hardware Kodek"),
+ ("Adaptive Bitrate", "Adaptív Bitrate"),
+ ("ID Server", "ID Szerver"),
+ ("Relay Server", "Relay Szerver"),
+ ("API Server", "API Szerver"),
+ ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."),
+ ("Invalid IP", "A megadott íp cím helytelen."),
+ ("id_change_tip", "Csak a-z, A-Z, 0-9 csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az első karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az ID hosszúsága 6-tól, 16 karakter."),
+ ("Invalid format", "Érvénytelen formátum"),
+ ("server_not_support", "Még nem támogatott a szerver által"),
+ ("Not available", "Nem érhető el"),
+ ("Too frequent", "Túl gyakori"),
+ ("Cancel", "Mégsem"),
+ ("Skip", "Kihagy"),
+ ("Close", "Bezár"),
+ ("Retry", "Újrapróbálkozás"),
+ ("OK", "OK"),
+ ("Password Required", "A jelszó megadása kötelező"),
+ ("Please enter your password", "Kérlek írd be a jelszavad"),
+ ("Remember password", "Kérlek emlékezz a jelszóra"),
+ ("Wrong Password", "Hibás jelszó"),
+ ("Do you want to enter again?", "Újra szeretnéd próbálni?"),
+ ("Connection Error", "Kapcsolódási Hiba"),
+ ("Error", "Hiba"),
+ ("Reset by the peer", "A kapcsolatot alaphelyzetbe állt"),
+ ("Connecting...", "Kapcsolódás..."),
+ ("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kérlek várj."),
+ ("Please try 1 minute later", "Kérlek próbáld újra 1 perc múlva."),
+ ("Login Error", "Belépési Hiba"),
+ ("Successful", "Sikeres"),
+ ("Connected, waiting for image...", "Kapcsolódva, várakozás a képre..."),
+ ("Name", "Név"),
+ ("Type", "Fajta"),
+ ("Modified", "Módosított"),
+ ("Size", "Méret"),
+ ("Show Hidden Files", "Rejtett Fájlok Mutatása"),
+ ("Receive", "Kapni"),
+ ("Send", "Küldeni"),
+ ("Refresh File", "Fájlok Frissítése"),
+ ("Local", "Lokális"),
+ ("Remote", "Távoli"),
+ ("Remote Computer", "Távoli Számítógép"),
+ ("Local Computer", "Lokális Számítógép"),
+ ("Confirm Delete", "Törlés Megerősítése"),
+ ("Delete", "Törlés"),
+ ("Properties", "Tulajdonságok"),
+ ("Multi Select", "Több fájl kiválasztása"),
+ ("Empty Directory", "Üres Könyvtár"),
+ ("Not an empty directory", "Nem egy üres könyvtár"),
+ ("Are you sure you want to delete this file?", "Biztosan törölni szeretnéd ezt a fájlt?"),
+ ("Are you sure you want to delete this empty directory?", "Biztosan törölni szeretnéd ezt az üres könyvtárat?"),
+ ("Are you sure you want to delete the file of this directory?", "Biztosan törölni szeretnéd a fájlokat ebben a könyvtárban?"),
+ ("Do this for all conflicts", "Ezt tedd az összes konfliktussal"),
+ ("This is irreversible!", "Ez a folyamat visszafordíthatatlan!"),
+ ("Deleting", "A törlés folyamatban"),
+ ("files", "fájlok"),
+ ("Waiting", "Várunk"),
+ ("Finished", "Végzett"),
+ ("Speed", "Gyorsaság"),
+ ("Custom Image Quality", "Egyedi Képminőség"),
+ ("Privacy mode", "Inkognító mód"),
+ ("Block user input", "Felhasználói input blokkokolása"),
+ ("Unblock user input", "Felhasználói input blokkolásának feloldása"),
+ ("Adjust Window", "Ablakméret beállítása"),
+ ("Original", "Eredeti"),
+ ("Shrink", "Zsugorított"),
+ ("Stretch", "Nyújtott"),
+ ("Good image quality", "Jó képminőség"),
+ ("Balanced", "Balanszolt"),
+ ("Optimize reaction time", "Válaszidő optimializálása"),
+ ("Custom", "Egyedi"),
+ ("Show remote cursor", "Távoli kurzor mutatása"),
+ ("Show quality monitor", "Minőségi monitor mutatása"),
+ ("Disable clipboard", "Vágólap Kikapcsolása"),
+ ("Lock after session end", "Lezárás a session végén"),
+ ("Insert", "Beszúrás"),
+ ("Insert Lock", "Beszúrási Zároló"),
+ ("Refresh", "Frissítés"),
+ ("ID does not exist", "Ez az ID nem létezik"),
+ ("Failed to connect to rendezvous server", "A randevú szerverhez való kapcsolódás sikertelen"),
+ ("Please try later", "Kérlek próbád később"),
+ ("Remote desktop is offline", "A távoli asztal offline"),
+ ("Key mismatch", "Eltérés a kulcsokban"),
+ ("Timeout", "Időtúllépés"),
+ ("Failed to connect to relay server", "A relay szerverhez való kapcsolódás sikertelen"),
+ ("Failed to connect via rendezvous server", "A randevú szerverrel való kapcsolódás sikertelen"),
+ ("Failed to connect via relay server", "A relay szerverrel való kapcsolódás sikertelen"),
+ ("Failed to make direct connection to remote desktop", "A távoli asztalhoz való direkt kapcsolódás sikertelen"),
+ ("Set Password", "Jelszó Beállítása"),
+ ("OS Password", "Operációs Rendszer Jelszavának Beállítása"),
+ ("install_tip", "Az UAC (Felhasználói Fiók Felügyelet) miatt, a RustDesk nem fog rendesen funkcionálni mint távoli oldal néhány esetben. Hogy ezt kikerüld, vagy kikapcsold, kérlek nyomj rá a gombra ezalatt az üzenet alatt, hogy feltelepítsd a RustDesket a rendszerre."),
+ ("Click to upgrade", "Kattints a frissítés telepítéséhez"),
+ ("Click to download", "Kattints a letöltéshez"),
+ ("Click to update", "Kattints a frissítés letöltéséhez"),
+ ("Configure", "Beállítás"),
+ ("config_acc", "Ahhoz hogy a RustDesket távolról irányítani tudd, \"Elérhetőségi\" jogokat kell adnod a RustDesk-nek."),
+ ("config_screen", "Ahhoz hogy a RustDesket távolról irányítani tudd, \"Képernyőfelvételi\" jogokat kell adnod a RustDesk-nek."),
+ ("Installing ...", "Telepítés..."),
+ ("Install", "Telepítés"),
+ ("Installation", "Telepítés"),
+ ("Installation Path", "Telepítési útvonal"),
+ ("Create start menu shortcuts", "Start menu parancsikon létrehozása"),
+ ("Create desktop icon", "Asztali icon létrehozása"),
+ ("agreement_tip", "Azzal hogy elindítod a telepítést, elfogadod a licenszszerződést."),
+ ("Accept and Install", "Elfogadás és Telepítés"),
+ ("End-user license agreement", "Felhasználói licencszerződés"),
+ ("Generating ...", "Generálás..."),
+ ("Your installation is lower version.", "A jelenleg feltelepített verzió régebbi."),
+ ("not_close_tcp_tip", "Ne zárd be ezt az ablakot miközben a tunnelt használod"),
+ ("Listening ...", "Halgazózás..."),
+ ("Remote Host", "Távoli Host"),
+ ("Remote Port", "Távoli Port"),
+ ("Action", "Akció"),
+ ("Add", "Add"),
+ ("Local Port", "Lokális Port"),
+ ("setup_server_tip", "Egy gyorsabb kapcsolatért, kérlek hostolj egy saját szervert"),
+ ("Too short, at least 6 characters.", "Túl rövid, legalább 6 karakter"),
+ ("The confirmation is not identical.", "A megerősítés nem volt azonos"),
+ ("Permissions", "Jogok"),
+ ("Accept", "Elfogad"),
+ ("Dismiss", "Elutasít"),
+ ("Disconnect", "Szétkapcsolás"),
+ ("Allow using keyboard and mouse", "Billentyűzet és egér használatának engedélyezése"),
+ ("Allow using clipboard", "Vágólap használatának engedélyezése"),
+ ("Allow hearing sound", "Hang átvitelének engedélyezése"),
+ ("Allow file copy and paste", "Fájlok másolásának és beillesztésének engedélyezése"),
+ ("Connected", "Kapcsolódva"),
+ ("Direct and encrypted connection", "Direkt, és titkosított kapcsolat"),
+ ("Relayed and encrypted connection", "Relayelt, és titkosított kapcsolat"),
+ ("Direct and unencrypted connection", "Direkt, és nem titkosított kapcsolat"),
+ ("Relayed and unencrypted connection", "Rekayelt, és nem titkosított kapcsolat"),
+ ("Enter Remote ID", "Kérlek írd be a távoli ID-t"),
+ ("Enter your password", "Kérlek írd be a jelszavadat"),
+ ("Logging in...", "A belépés folyamatban..."),
+ ("Enable RDP session sharing", "Az RDP session megosztás engedélyezése"),
+ ("Auto Login", "Automatikus Login"),
+ ("Enable Direct IP Access", "Direkt IP elérés engedélyezése"),
+ ("Rename", "Átnevezés"),
+ ("Space", "Hely"),
+ ("Create Desktop Shortcut", "Asztali Parancsikon Lértehozása"),
+ ("Change Path", "Útvonal Megváltoztatása"),
+ ("Create Folder", "Mappa Készítése"),
+ ("Please enter the folder name", "Kérlek írd be a mappa nevét"),
+ ("Fix it", "Kérlek javísd meg"),
+ ("Warning", "Figyelem"),
+ ("Login screen using Wayland is not supported", "A belépési kijelzővel a Wayland használata nem támogatott"),
+ ("Reboot required", "Újraindítás szükséges"),
+ ("Unsupported display server ", "Nem támogatott kijelző szerver"),
+ ("x11 expected", "x11-re számítottt"),
+ ("Port", "Port"),
+ ("Settings", "Beállítások"),
+ ("Username", "Felhasználónév"),
+ ("Invalid port", "Érvénytelen port"),
+ ("Closed manually by the peer", "A kapcsolat manuálisan be lett zárva a másik fél álltal"),
+ ("Enable remote configuration modification", "Távoli konfiguráció módosítás engedélyezése"),
+ ("Run without install", "Futtatás feltelepítés nélkül"),
+ ("Always connected via relay", "Mindig relay által kapcsolódott"),
+ ("Always connect via relay", "Mindig relay által kapcsolódik"),
+ ("whitelist_tip", "Csak a fehérlistán lévő címek érhetnek el"),
+ ("Login", "Belépés"),
+ ("Logout", "Kilépés"),
+ ("Tags", "Tagok"),
+ ("Search ID", "ID keresés"),
+ ("Current Wayland display server is not supported", "Jelenleg a Wayland display szerver nem támogatott"),
+ ("whitelist_sep", "Ide jönnek a címek, vesző, pontosvessző, space, vagy új sorral elválasztva"),
+ ("Add ID", "ID Hozzáadása"),
+ ("Add Tag", "Tag Hozzáadása"),
+ ("Unselect all tags", "Az összes tag kiválasztásának törlése"),
+ ("Network error", "Hálózati hiba"),
+ ("Username missed", "A felhasználónév kimaradt"),
+ ("Password missed", "A jelszó kimaradt"),
+ ("Wrong credentials", "Hibás felhasználónév vagy jelszó"),
+ ("Edit Tag", "A tag(ok) szerkeztése"),
+ ("Unremember Password", "A jelszó megjegyzésének törlése"),
+ ("Favorites", "Kedvencek"),
+ ("Add to Favorites", "Hozzáadás a kedvencekhez"),
+ ("Remove from Favorites", "Eltávolítás a kedvencektől"),
+ ("Empty", "Üres"),
+ ("Invalid folder name", "Helytelen fájlnév"),
+ ("Socks5 Proxy", "Socks5-ös Proxy"),
+ ("Hostname", "Hostnév"),
+ ("Discovered", "Felfedezés"),
+ ("install_daemon_tip", "Ahhoz hogy a RustDesk bootkor elinduljon, telepítened kell a rendszer szolgáltatást."),
+ ("Remote ID", "Távoli ID"),
+ ("Paste", "Beillesztés"),
+ ("Paste here?", "Beillesztés ide?"),
+ ("Are you sure to close the connection?", "Biztos vagy benne hogy be szeretnéd zárni a kapcsolatot?"),
+ ("Download new version", "Új verzó letöltése"),
+ ("Touch mode", "Érintési mód bekapcsolása"),
+ ("Mouse mode", "Egérhasználati mód bekapcsolása"),
+ ("One-Finger Tap", "Egyújas érintés"),
+ ("Left Mouse", "Baloldali Egér"),
+ ("One-Long Tap", "Egy hosszú érintés"),
+ ("Two-Finger Tap", "Két újas érintés"),
+ ("Right Mouse", "Jobboldali Egér"),
+ ("One-Finger Move", "Egyújas mozgatás"),
+ ("Double Tap & Move", "Kétszeri érintés, és Mozgatás"),
+ ("Mouse Drag", "Egérrel való húzás"),
+ ("Three-Finger vertically", "Három ujj függőlegesen"),
+ ("Mouse Wheel", "Egérgörgő"),
+ ("Two-Finger Move", "Kátújas mozgatás"),
+ ("Canvas Move", "Nézet Mozgatása"),
+ ("Pinch to Zoom", "Húzd össze a nagyításhoz"),
+ ("Canvas Zoom", "Nézet Nagyítása"),
+ ("Reset canvas", "Nézet visszaállítása"),
+ ("No permission of file transfer", "Nincs jogod fájl transzer indításához"),
+ ("Note", "Megyjegyzés"),
+ ("Connection", "Kapcsolat"),
+ ("Share Screen", "Képernyőmegosztás"),
+ ("CLOSE", "LETILT"),
+ ("OPEN", "ENGEDÉLYEZ"),
+ ("Chat", "Chat"),
+ ("Total", "Összes"),
+ ("items", "Tárgyak"),
+ ("Selected", "Kiválasztott"),
+ ("Screen Capture", "Képernyőrögzítés"),
+ ("Input Control", "Input Kontrol"),
+ ("Audio Capture", "Audió Rögzítés"),
+ ("File Connection", "Fájlkapcsolat"),
+ ("Screen Connection", "Új Vizuális Kapcsolat"),
+ ("Do you accept?", "Elfogadod?"),
+ ("Open System Setting", "Rendszer beállítások megnyitása"),
+ ("How to get Android input permission?", "Hogyan állíthatok be Android input jogokat?"),
+ ("android_input_permission_tip1", "Ahhoz hogy egy távoli eszköz kontolálhassa az Android eszközödet egérrel vagy érintéssel, jogot kell adnod a RustDesk-nek, hogy használja az \"Elérhetőségi\" szolgáltatást."),
+ ("android_input_permission_tip2", "Kérlek navigálj a rendszer beállításaihoz, keresd meg vagy írd be hogy [Feltelepített Szolgáltatások], és kapcsold be a [RustDesk Input] szolgáltatást."),
+ ("android_new_connection_tip", "Új kontrollálási kérés érkezett, amely irányítani szeretné az eszközöded."),
+ ("android_service_will_start_tip", "A \"Képernyőrögzítés\" engedélyezése automatikusan elindítja majd a szolgáltatást, amely megengedi más eszközöknek hogy kérést kezdeményezzenek az eszköz felé."),
+ ("android_stop_service_tip", "A szolgáltatás bezárása automatikusan szétkapcsol minden létező kapcsolatot."),
+ ("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, kérlek frissíts legalább Android 10-re, vagy egy újabb verzióra."),
+ ("android_start_service_tip", "Nyomj a [Szolgáltatás Indítása] opcióra, vagy adj [Képernyőrözítési] jogot az applikációnak hogy elindítsd a képernyőmegosztó szolgáltatást."),
+ ("Account", "Fiók"),
+ ("Overwrite", "Felülírás"),
+ ("This file exists, skip or overwrite this file?", "Ez a fájl már létezik, skippeljünk, vagy felülírjuk ezt a fájlt?"),
+ ("Quit", "Kilépés"),
+ ("doc_mac_permission", "https://rustdesk.com/docs/hu/manual/mac/#enable-permissions"),
+ ("Help", "Segítség"),
+ ("Failed", "Sikertelen"),
+ ("Succeeded", "Sikeres"),
+ ("Someone turns on privacy mode, exit", "Valaki bekacsolta a privát módot, lépj ki"),
+ ("Unsupported", "Nem támogatott"),
+ ("Peer denied", "Elutasítva a távoli fél álltal"),
+ ("Please install plugins", "Kérlek telepítsd a pluginokat"),
+ ("Peer exit", "A távoli fél kilépett"),
+ ("Failed to turn off", "Nem tudtuk kikapcsolni"),
+ ("Turned off", "Kikapcsolva"),
+ ("In privacy mode", "Belépés a privát módba"),
+ ("Out privacy mode", "Kilépés a privát módból"),
+ ("Language", "Nyelv"),
+ ("Keep RustDesk background service", ""),
+ ("Ignore Battery Optimizations", ""),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", ""),
+ ("Use temporary password", ""),
+ ("Use permanent password", ""),
+ ("Use both passwords", ""),
+ ("Set permanent password", ""),
+ ("Set temporary password length", ""),
+ ("Enable Remote Restart", ""),
+ ("Allow remote restart", ""),
+ ("Restart Remote Device", ""),
+ ("Are you sure you want to restart", ""),
+ ("Restarting Remote Device", ""),
+ ("remote_restarting_tip", ""),
+ ].iter().cloned().collect();
+}
diff --git a/src/lang/id.rs b/src/lang/id.rs
index 2e2f2fd1c..62cb7b6b5 100644
--- a/src/lang/id.rs
+++ b/src/lang/id.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "Tentang"),
("Mute", "Bisukan"),
("Audio Input", "Masukkan Audio"),
+ ("Enhancements", "Peningkatan"),
+ ("Hardware Codec", "Codec Perangkat Keras"),
+ ("Adaptive Bitrate", "Kecepatan Bitrate Adaptif"),
("ID Server", "Server ID"),
("Relay Server", "Server Relay"),
("API Server", "API Server"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimalkan waktu reaksi"),
("Custom", "Custom"),
("Show remote cursor", "Tampilkan remote kursor"),
+ ("Show quality monitor", ""),
("Disable clipboard", "Matikan papan klip"),
("Lock after session end", "Kunci setelah sesi berakhir"),
("Insert", "Menyisipkan"),
@@ -158,7 +162,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Allow using clipboard", "Izinkan menggunakan papan klip"),
("Allow hearing sound", "Izinkan mendengarkan suara"),
("Allow file copy and paste", "Izinkan penyalinan dan tempel file"),
- ("File transfer", "Transfer file"),
("Connected", "Terkoneksi"),
("Direct and encrypted connection", "Koneksi langsung dan terenkripsi"),
("Relayed and encrypted connection", "Koneksi relai dan terenkripsi"),
@@ -265,7 +268,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_start_service_tip", "Ketuk izin [Mulai Layanan] atau BUKA [Tangkapan Layar] untuk memulai layanan berbagi layar."),
("Account", "Akun"),
("Overwrite", "Timpa"),
- ("This file exists, skip or overwrite this file?", ""),
+ ("This file exists, skip or overwrite this file?", "File ini sudah ada, lewati atau timpa file ini?"),
("Quit", "Keluar"),
("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"),
("Help", "Bantuan"),
@@ -280,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Matikan"),
("In privacy mode", "Dalam mode privasi"),
("Out privacy mode", "Keluar dari mode privasi"),
+ ("Language", "Bahasa"),
+ ("Keep RustDesk background service", "Pertahankan RustDesk berjalan pada background service"),
+ ("Ignore Battery Optimizations", "Abaikan Pengoptimalan Baterai"),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", "Koneksi tidak dijinkan"),
+ ("Use temporary password", "Gunakan kata sandi sementara"),
+ ("Use permanent password", "Gunakan kata sandi permanaen"),
+ ("Use both passwords", "Gunakan kedua kata sandi "),
+ ("Set permanent password", "Setel kata sandi permanen"),
+ ("Set temporary password length", "Setel panjang kata sandi sementara"),
+ ("Enable Remote Restart", "Aktifkan Restart Jarak Jauh"),
+ ("Allow remote restart", "Ijinkan Restart Jarak Jauh"),
+ ("Restart Remote Device", "Restart Perangkat Jarak Jauh"),
+ ("Are you sure you want to restart", "Apakah Anda yakin untuk memulai ulang"),
+ ("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"),
+ ("remote_restarting_tip", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/it.rs b/src/lang/it.rs
index 4bf692169..8058806c2 100644
--- a/src/lang/it.rs
+++ b/src/lang/it.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "Informazioni"),
("Mute", "Silenzia"),
("Audio Input", "Input audio"),
+ ("Enhancements", ""),
+ ("Hardware Codec", ""),
+ ("Adaptive Bitrate", ""),
("ID Server", "ID server"),
("Relay Server", "Server relay"),
("API Server", "Server API"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Ottimizza il tempo di reazione"),
("Custom", "Personalizzato"),
("Show remote cursor", "Mostra il cursore remoto"),
+ ("Show quality monitor", ""),
("Disable clipboard", "Disabilita appunti"),
("Lock after session end", "Blocca al termine della sessione"),
("Insert", "Inserisci"),
@@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Spegni"),
("In privacy mode", "In modalità privacy"),
("Out privacy mode", "Fuori modalità privacy"),
+ ("Language", "Linguaggio"),
+ ("Keep RustDesk background service", ""),
+ ("Ignore Battery Optimizations", ""),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", ""),
+ ("Use temporary password", ""),
+ ("Use permanent password", ""),
+ ("Use both passwords", ""),
+ ("Set permanent password", ""),
+ ("Set temporary password length", ""),
+ ("Enable Remote Restart", ""),
+ ("Allow remote restart", ""),
+ ("Restart Remote Device", ""),
+ ("Are you sure you want to restart", ""),
+ ("Restarting Remote Device", ""),
+ ("remote_restarting_tip", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/pl.rs b/src/lang/pl.rs
new file mode 100644
index 000000000..71b753ac3
--- /dev/null
+++ b/src/lang/pl.rs
@@ -0,0 +1,304 @@
+lazy_static::lazy_static! {
+pub static ref T: std::collections::HashMap<&'static str, &'static str> =
+ [
+ ("Status", "Status"),
+ ("Your Desktop", "Twój pulpit"),
+ ("desk_tip", "Podpowiedź"),
+ ("Password", "Hasło"),
+ ("Ready", "Gotowe"),
+ ("Established", "Ustawiony"),
+ ("connecting_status", "Status połączenia"),
+ ("Enable Service", "Włącz usługę"),
+ ("Start Service", "Uruchom usługę"),
+ ("Service is running", "Usługa uruchomiona"),
+ ("Service is not running", "Usługa nie jest uruchomiona"),
+ ("not_ready_status", "Brak gotowości"),
+ ("Control Remote Desktop", "Kontroluj zdalny pulpit"),
+ ("Transfer File", "Prześlij plik"),
+ ("Connect", "Połącz"),
+ ("Recent Sessions", "Ostatnie sesje"),
+ ("Address Book", "Książka adresowa"),
+ ("Confirmation", "Potwierdzenie"),
+ ("TCP Tunneling", "Tunelowanie TCP"),
+ ("Remove", "Usuń"),
+ ("Refresh random password", "Odśwież losowe hasło"),
+ ("Set your own password", "Ustaw własne hasło"),
+ ("Enable Keyboard/Mouse", "Włącz klawiaturę/mysz"),
+ ("Enable Clipboard", "Włącz schowek"),
+ ("Enable File Transfer", "Włącz przesyłanie pliku"),
+ ("Enable TCP Tunneling", "Włącz tunelowanie TCP"),
+ ("IP Whitelisting", "Biała lista IP"),
+ ("ID/Relay Server", "ID/Relay Serwer"),
+ ("Stop service", "Zatrzymaj usługę"),
+ ("Change ID", "Zmień ID"),
+ ("Website", "Strona internetowa"),
+ ("About", "O"),
+ ("Mute", "Wycisz"),
+ ("Audio Input", "Wejście audio"),
+ ("Enhancements", "Ulepszenia"),
+ ("Hardware Codec", "Kodek sprzętowy"),
+ ("Adaptive Bitrate", "Adaptacyjny bitrate"),
+ ("ID Server", "ID Serwer"),
+ ("Relay Server", "Relay serwer"),
+ ("API Server", "API Serwer"),
+ ("invalid_http", "Nieprawidłowy http"),
+ ("Invalid IP", "Nieprawidłowy IP"),
+ ("id_change_tip", "Podpowiedź zmiany ID"),
+ ("Invalid format", "Nieprawidłowy format"),
+ ("server_not_support", "Serwer nie wspiera"),
+ ("Not available", "Niedostępne"),
+ ("Too frequent", "Zbyt często"),
+ ("Cancel", "Anuluj"),
+ ("Skip", "Pomiń"),
+ ("Close", "Zamknij"),
+ ("Retry", "Ponów"),
+ ("OK", "OK"),
+ ("Password Required", "Wymagane hasło"),
+ ("Please enter your password", "Wpisz proszę twoje hasło"),
+ ("Remember password", "Zapamiętaj hasło"),
+ ("Wrong Password", "Błędne hasło"),
+ ("Do you want to enter again?", "Chcesz wprowadzić ponownie?"),
+ ("Connection Error", "Błąd połączenia"),
+ ("Error", "Błąd"),
+ ("Reset by the peer", "Zresetuj przez peer"),
+ ("Connecting...", "Łączenie..."),
+ ("Connection in progress. Please wait.", "Trwa łączenie. Proszę czekać."),
+ ("Please try 1 minute later", "Spróbuj za minutę"),
+ ("Login Error", "Błąd logowania"),
+ ("Successful", "Sukces"),
+ ("Connected, waiting for image...", "Połączono, czekam na obraz..."),
+ ("Name", "Nazwa"),
+ ("Type", "Typ"),
+ ("Modified", "Zmodyfikowany"),
+ ("Size", "Rozmiar"),
+ ("Show Hidden Files", "Pokaż ukryte pliki"),
+ ("Receive", "Odbierz"),
+ ("Send", "Wyślij"),
+ ("Refresh File", "Odśwież plik"),
+ ("Local", "Lokalny"),
+ ("Remote", "Zdalny"),
+ ("Remote Computer", "Zdalny komputer"),
+ ("Local Computer", "Lokalny komputer"),
+ ("Confirm Delete", "Potwierdź usunięcie"),
+ ("Delete", "Usuń"),
+ ("Properties", "Właściwości"),
+ ("Multi Select", "Wielokrotny wybór"),
+ ("Empty Directory", "Pusty katalog"),
+ ("Not an empty directory", "Katalog nie jest pusty"),
+ ("Are you sure you want to delete this file?", "Czy na pewno chcesz usunąć ten plik?"),
+ ("Are you sure you want to delete this empty directory?", ""),
+ ("Are you sure you want to delete the file of this directory?", "Czy na pewno chcesz usunąć ten pusty katalog?"),
+ ("Do this for all conflicts", "Zrób to dla wszystkich konfliktów"),
+ ("This is irreversible!", "To jest nieodwracalne!"),
+ ("Deleting", "Usuwanie"),
+ ("files", "pliki"),
+ ("Waiting", "Czekanie"),
+ ("Finished", "Zakończono"),
+ ("Speed", "Prędkość"),
+ ("Custom Image Quality", "Domyślna jakość obrazu"),
+ ("Privacy mode", "Tryb prywatny"),
+ ("Block user input", "Blokuj wprowadzanie"),
+ ("Unblock user input", "Odblokuj wprowadzanie"),
+ ("Adjust Window", "Dostosuj okno"),
+ ("Original", "Oryginał"),
+ ("Shrink", "Zmniejsz"),
+ ("Stretch", "Zwiększ"),
+ ("Good image quality", "Dobra jakość obrazu"),
+ ("Balanced", "Zrównoważony"),
+ ("Optimize reaction time", "Zoptymalizuj czas reakcji"),
+ ("Custom", "Niestandardowy"),
+ ("Show remote cursor", "Pokazuj zdalny kursor"),
+ ("Show quality monitor", "Pokazuj jakość monitora"),
+ ("Disable clipboard", "Wyłącz schowek"),
+ ("Lock after session end", "Zablokuj po zakończeniu sesji"),
+ ("Insert", "Wstaw"),
+ ("Insert Lock", "Wstaw blokadę"),
+ ("Refresh", "Odśwież"),
+ ("ID does not exist", "ID nie istnieje"),
+ ("Failed to connect to rendezvous server", "Nie udało się połączyć z serwerem spotkania"),
+ ("Please try later", "Spróbuj później"),
+ ("Remote desktop is offline", "Zdalny pulpit jest off-line"),
+ ("Key mismatch", "Niezgodność klucza"),
+ ("Timeout", "Przekroczenie czasu"),
+ ("Failed to connect to relay server", "Nie udało się połączyć z serwerem pośredniczącym"),
+ ("Failed to connect via rendezvous server", "Nie udało się połączyć przez serwer spotkań"),
+ ("Failed to connect via relay server", "Nie udało się połączyć przez serwer pośredniczący"),
+ ("Failed to make direct connection to remote desktop", "Nie udało się nawiązać bezpośredniego połączenia ze zdalnym pulpitem"),
+ ("Set Password", "Ustaw hasło"),
+ ("OS Password", "Hasło systemu operacyjnego"),
+ ("install_tip", "Podpowiedź do instalacji"),
+ ("Click to upgrade", "Kliknij, aby uaktualnić (upgrade)"),
+ ("Click to download", "Kliknij, aby pobrać"),
+ ("Click to update", "Kliknij, aby zaktualizować (update)"),
+ ("Configure", "Konfiguruj"),
+ ("config_acc", ""),
+ ("config_screen", "Konfiguracja konta"),
+ ("Installing ...", "Instalowanie..."),
+ ("Install", "Instaluj"),
+ ("Installation", "Instalacja"),
+ ("Installation Path", "Ścieżka instalacji"),
+ ("Create start menu shortcuts", "Utwórz skrót w menu"),
+ ("Create desktop icon", "Utwórz ikonę na pulpicie"),
+ ("agreement_tip", "Podpowiedź do umowy/zgody"),
+ ("Accept and Install", "Akceptuj i instaluj"),
+ ("End-user license agreement", "Umowa licencyjna użytkownika końcowego"),
+ ("Generating ...", "Generowanie..."),
+ ("Your installation is lower version.", "Twoja instalacja to niższa wersja"),
+ ("not_close_tcp_tip", "Not close TCP tip"),
+ ("Listening ...", "Słucham..."),
+ ("Remote Host", "Zdalny host"),
+ ("Remote Port", "Zdalny port"),
+ ("Action", "Akcja"),
+ ("Add", "Dodaj"),
+ ("Local Port", "Lokalny port"),
+ ("setup_server_tip", "Podpowiedź do ustawień serwera"),
+ ("Too short, at least 6 characters.", "Za krótkie, min. 6 znaków"),
+ ("The confirmation is not identical.", "Potwierdzenie nie jest identyczne."),
+ ("Permissions", "Uprawnienia"),
+ ("Accept", "Akceptuj"),
+ ("Dismiss", "Odrzuć"),
+ ("Disconnect", "Rozłącz"),
+ ("Allow using keyboard and mouse", "Zezwalaj na używanie klawiatury i myszy"),
+ ("Allow using clipboard", "Zezwalaj na używanie schowka"),
+ ("Allow hearing sound", "Pozwól słyszeć dźwięk"),
+ ("Allow file copy and paste", "Zezwalaj na kopiowanie i wklejanie plików"),
+ ("Connected", "Połączony"),
+ ("Direct and encrypted connection", "Bezpośrednie i szyfrowane połączenie"),
+ ("Relayed and encrypted connection", "Połączenie przekazywane i szyfrowane"),
+ ("Direct and unencrypted connection", "Połączenie bezpośrednie i nieszyfrowane"),
+ ("Relayed and unencrypted connection", "Połączenie przekazywane i nieszyfrowane"),
+ ("Enter Remote ID", "Wprowadź zdalne ID"),
+ ("Enter your password", "Wprowadź hasło"),
+ ("Logging in...", "Loguję się ..."),
+ ("Enable RDP session sharing", "Włącz udostępnianie sesji RDP"),
+ ("Auto Login", "Automatyczne logowanie"),
+ ("Enable Direct IP Access", "Włącz bezpośredni dostęp IP"),
+ ("Rename", "Zmień nazwę"),
+ ("Space", "Przestrzeń"),
+ ("Create Desktop Shortcut", "Utwórz skrót na pulpicie"),
+ ("Change Path", "Zmień ścieżkę"),
+ ("Create Folder", "Utwórz folder"),
+ ("Please enter the folder name", "Wpisz nazwę folderu"),
+ ("Fix it", "Napraw to"),
+ ("Warning", "Ostrzeżenie"),
+ ("Login screen using Wayland is not supported", "Ekran logowania przy użyciu Wayland nie jest obsługiwany"),
+ ("Reboot required", "Wymagany restart"),
+ ("Unsupported display server ", "Nieobsługiwany serwer wyświetlania "),
+ ("x11 expected", "oczekiwane X11"),
+ ("Port", "Port"),
+ ("Settings", "Ustawienia"),
+ ("Username", "Nazwa użytkownika"),
+ ("Invalid port", "Nieprawidłowy port"),
+ ("Closed manually by the peer", "Zamykany ręcznie przez peer"),
+ ("Enable remote configuration modification", "Włącz zdalną modyfikację konfiguracji"),
+ ("Run without install", "Uruchom bez instalacji"),
+ ("Always connected via relay", "Zawsze połączony pośrednio"),
+ ("Always connect via relay", "Zawsze łącz pośrednio"),
+ ("whitelist_tip", "Podpowiedź do białej listy"),
+ ("Login", "Zaloguj"),
+ ("Logout", "Wyloguj"),
+ ("Tags", "Tagi"),
+ ("Search ID", "Szukaj ID"),
+ ("Current Wayland display server is not supported", "Obecny serwer wyświetlania Wayland nie jest obsługiwany"),
+ ("whitelist_sep", "whitelist_sep"),
+ ("Add ID", "Dodaj ID"),
+ ("Add Tag", "Dodaj Tag"),
+ ("Unselect all tags", "Odznacz wszystkie tagi"),
+ ("Network error", "Błąd sieci"),
+ ("Username missed", "Brak użytkownika"),
+ ("Password missed", "Brak hasła"),
+ ("Wrong credentials", "Błędne dane uwierzytelniające"),
+ ("Edit Tag", "Edytuj tag"),
+ ("Unremember Password", "Zapomnij hasło"),
+ ("Favorites", "Ulubione"),
+ ("Add to Favorites", "Dodaj do ulubionych"),
+ ("Remove from Favorites", "Usuń z ulubionych"),
+ ("Empty", "Pusty"),
+ ("Invalid folder name", "Błędna nazwa folderu"),
+ ("Socks5 Proxy", "Socks5 Proxy"),
+ ("Hostname", "Nazwa hosta"),
+ ("Discovered", "Discovered"),
+ ("install_daemon_tip", "Podpowiedź instalacji daemona"),
+ ("Remote ID", "Zdalne ID"),
+ ("Paste", "Wklej"),
+ ("Paste here?", "Wkleić tu?"),
+ ("Are you sure to close the connection?", "Czy na pewno chcesz zamknąć połączenie?"),
+ ("Download new version", "Pobierz nową wersję"),
+ ("Touch mode", "Tryb dotykowy"),
+ ("Mouse mode", "Tryb myszy"),
+ ("One-Finger Tap", "Dotknij jednym palcem"),
+ ("Left Mouse", "Mysz dla leworęcznych"),
+ ("One-Long Tap", "Przytrzymaj jednym palcem"),
+ ("Two-Finger Tap", "Dotknij dwoma palcami"),
+ ("Right Mouse", "Mysz dla praworęcznych"),
+ ("One-Finger Move", "Ruch jednym palcem"),
+ ("Double Tap & Move", "Dotknij dwukrotnie i przesuń"),
+ ("Mouse Drag", "Przeciągnij myszą"),
+ ("Three-Finger vertically", "Trzy palce wertykalnie"),
+ ("Mouse Wheel", "Kółko myszy"),
+ ("Two-Finger Move", "Ruch dwoma palcami"),
+ ("Canvas Move", "Canvas Move"),
+ ("Pinch to Zoom", "Uszczypnij, aby powiększyć"),
+ ("Canvas Zoom", "Canvas Zoom"),
+ ("Reset canvas", "Reset canvas"),
+ ("No permission of file transfer", "Brak uprawnień na przesyłanie plików"),
+ ("Note", "Notatka"),
+ ("Connection", "Połączenie"),
+ ("Share Screen", "Udostępnij ekran"),
+ ("CLOSE", "Zamknij"),
+ ("OPEN", "Otwórz"),
+ ("Chat", "Czat"),
+ ("Total", "Total"),
+ ("items", "przedmioty"),
+ ("Selected", "Zaznaczone"),
+ ("Screen Capture", "Przechwyć ekran"),
+ ("Input Control", "Kontrola wejścia"),
+ ("Audio Capture", "Przechwyć dźwięk"),
+ ("File Connection", "File Connection"),
+ ("Screen Connection", "Screen Connection"),
+ ("Do you accept?", "Akceptujesz?"),
+ ("Open System Setting", "Otwórz ustawienia systemowe"),
+ ("How to get Android input permission?", "Jak uzyskać uprawnienia do wprowadzania danych w systemie Android?"),
+ ("android_input_permission_tip1", "android_input_permission_tip1"),
+ ("android_input_permission_tip2", "android_input_permission_tip2"),
+ ("android_new_connection_tip", "android_new_connection_tip"),
+ ("android_service_will_start_tip", "android_service_will_start_tip"),
+ ("android_stop_service_tip", "android_stop_service_tip"),
+ ("android_version_audio_tip", "android_version_audio_tip"),
+ ("android_start_service_tip", "android_start_service_tip"),
+ ("Account", "Konto"),
+ ("Overwrite", "Nadpisz"),
+ ("This file exists, skip or overwrite this file?", "Ten plik istnieje, pominąć czy nadpisać ten plik?"),
+ ("Quit", "Zrezygnuj"),
+ ("doc_mac_permission", "doc_mac_permission"),
+ ("Help", "Pomoc"),
+ ("Failed", "Niepowodzenie"),
+ ("Succeeded", "Udało się"),
+ ("Someone turns on privacy mode, exit", "Ktoś włącza tryb prywatności, wyjdź"),
+ ("Unsupported", "NIewspierane"),
+ ("Peer denied", "Odmowa peer"),
+ ("Please install plugins", "Zainstaluj plugin"),
+ ("Peer exit", "Wyjście peer"),
+ ("Failed to turn off", "Nie udało się wyłączyć"),
+ ("Turned off", "Wyłączony"),
+ ("In privacy mode", "Wejdź w tryb prywatności"),
+ ("Out privacy mode", "Opuść tryb prywatności"),
+ ("Language", "Język"),
+ ("Keep RustDesk background service", "Zachowaj usługę w tle RustDesk"),
+ ("Ignore Battery Optimizations", "Ignoruj optymalizację baterii"),
+ ("android_open_battery_optimizations_tip", "android_open_battery_optimizations_tip"),
+ ("Random Password After Session", "Losowe hasło po sesji"),
+ ("Keep", "Zachowaj"),
+ ("Update", "Aktualizacja"),
+ ("Disable", "Wyłącz"),
+ ("Onetime Password", "Hasło jednorazowe"),
+ ("Verification Method", "Metoda weryfikacji"),
+ ("Enable security password", "Włącz hasło zabezpieczające"),
+ ("Enable random password", "Włącz losowe hasło"),
+ ("Enable onetime password", "Włącz hasło jednorazowe"),
+ ("Disable onetime password", "Wyłącz hasło jednorazowe"),
+ ("Activate onetime password", "Aktywuj hasło jednorazowe"),
+ ("Set security password", "Ustaw hasło zabezpieczające"),
+ ("Connection not allowed", "Połączenie niedozwolone"),
+ ].iter().cloned().collect();
+}
diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs
index e51d5f5f9..0aac8bc8a 100644
--- a/src/lang/ptbr.rs
+++ b/src/lang/ptbr.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "Sobre"),
("Mute", "Emudecer"),
("Audio Input", "Entrada de Áudio"),
+ ("Enhancements", ""),
+ ("Hardware Codec", ""),
+ ("Adaptive Bitrate", ""),
("ID Server", "Servidor de ID"),
("Relay Server", "Servidor de Relay"),
("API Server", "Servidor da API"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Otimizar tempo de reação"),
("Custom", "Personalizado"),
("Show remote cursor", "Mostrar cursor remoto"),
+ ("Show quality monitor", ""),
("Disable clipboard", "Desabilitar área de transferência"),
("Lock after session end", "Bloquear após o fim da sessão"),
("Insert", "Inserir"),
@@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Desligado"),
("In privacy mode", "No modo de privacidade"),
("Out privacy mode", "Fora do modo de privacidade"),
+ ("Language", ""),
+ ("Keep RustDesk background service", ""),
+ ("Ignore Battery Optimizations", ""),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", ""),
+ ("Use temporary password", ""),
+ ("Use permanent password", ""),
+ ("Use both passwords", ""),
+ ("Set permanent password", ""),
+ ("Set temporary password length", ""),
+ ("Enable Remote Restart", ""),
+ ("Allow remote restart", ""),
+ ("Restart Remote Device", ""),
+ ("Are you sure you want to restart", ""),
+ ("Restarting Remote Device", ""),
+ ("remote_restarting_tip", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/ru.rs b/src/lang/ru.rs
index 88245b1d0..0b18683a2 100644
--- a/src/lang/ru.rs
+++ b/src/lang/ru.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "О RustDesk"),
("Mute", "Отключить звук"),
("Audio Input", "Аудиовход"),
+ ("Enhancements", "Улучшения"),
+ ("Hardware Codec", "Аппаратный кодек"),
+ ("Adaptive Bitrate", "Адаптивная скорость потока"),
("ID Server", "ID-сервер"),
("Relay Server", "Сервер ретрансляции"),
("API Server", "API-сервер"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Оптимизировать время реакции"),
("Custom", "Пользовательский"),
("Show remote cursor", "Показать удаленный курсор"),
+ ("Show quality monitor", "Показать качество"),
("Disable clipboard", "Отключить буфер обмена"),
("Lock after session end", "Выход из учётной записи после завершения сеанса"),
("Insert", "Вставить"),
@@ -121,9 +125,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Failed to connect via relay server", "Не удалось подключиться через сервер ретрансляции"),
("Failed to make direct connection to remote desktop", "Не удалось установить прямое подключение к удаленному рабочему столу"),
("Set Password", "Установить пароль"),
- ("OS Password", "Пароль операционной системы"),
+ ("OS Password", "Пароль ОС"),
("install_tip", "В некоторых случаях из-за UAC RustDesk может работать некорректно на удаленном узле. Чтобы избежать UAC, нажмите кнопку ниже, чтобы установить RustDesk в системе"),
- ("Click to upgrade", "Нажмите, чтобы проверить на наличие обновлений"),
+ ("Click to upgrade", "Нажмите, чтобы проверить наличие обновлений"),
("Click to download", "Нажмите, чтобы скачать"),
("Click to update", "Нажмите, чтобы обновить"),
("Configure", "Настроить"),
@@ -132,14 +136,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Installing ...", "Устанавливается..."),
("Install", "Установить"),
("Installation", "Установка"),
- ("Installation Path", "Папка установки"),
+ ("Installation Path", "Путь установки"),
("Create start menu shortcuts", "Создать ярлыки меню \"Пуск\""),
("Create desktop icon", "Создать значок на рабочем столе"),
- ("agreement_tip", "Если вы начнете установку, примите лицензионное соглашение"),
+ ("agreement_tip", "Начиная установку, вы принимаете условия лицензионного соглашения"),
("Accept and Install", "Принять и установить"),
("End-user license agreement", "Лицензионное соглашение с конечным пользователем"),
("Generating ...", "Генерация..."),
- ("Your installation is lower version.", "Ваша инсталяция является более ранней версией"),
+ ("Your installation is lower version.", "Ваша установка более ранней версии"),
("not_close_tcp_tip", "Не закрывать это окно при использовании туннеля"),
("Listening ...", "Ожидаем..."),
("Remote Host", "Удаленная машина"),
@@ -159,11 +163,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Allow hearing sound", "Разрешить передачу звука"),
("Allow file copy and paste", "Разрешить копирование и вставку файлов"),
("Connected", "Подключено"),
- ("Direct and encrypted connection", "Прямое и шифрованное соединение"),
- ("Relayed and encrypted connection", "Коммутируемое и зашифрованное соединение"),
+ ("Direct and encrypted connection", "Прямое и зашифрованное соединение"),
+ ("Relayed and encrypted connection", "Ретранслируемое и зашифрованное соединение"),
("Direct and unencrypted connection", "Прямое и незашифрованное соединение"),
- ("Relayed and unencrypted connection", "Коммутируемое и незашифрованное соединение"),
- ("Enter Remote ID", "Введите удаленный идентификатор"),
+ ("Relayed and unencrypted connection", "Ретранслируемое и незашифрованное соединение"),
+ ("Enter Remote ID", "Введите удаленный ID"),
("Enter your password", "Введите пароль"),
("Logging in...", "Вход..."),
("Enable RDP session sharing", "Включить общий доступ к сеансу RDP"),
@@ -218,13 +222,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Remote ID", "Удаленный идентификатор"),
("Paste", "Вставить"),
("Paste here?", "Вставить сюда?"),
- ("Are you sure to close the connection?", "Вы уверены, что хотите закрыть соединение?"),
- ("Download new version", "Загрузить новую версию"),
+ ("Are you sure to close the connection?", "Вы уверены, что хотите завершить подключение?"),
+ ("Download new version", "Скачать новую версию"),
("Touch mode", "Сенсорный режим"),
("Mouse mode", "Режим мыши"),
("One-Finger Tap", "Касание одним пальцем"),
("Left Mouse", "Левая кнопка мыши"),
- ("One-Long Tap", "Одно долгое касание пальцем"),
+ ("One-Long Tap", "Одно долгое нажатие пальцем"),
("Two-Finger Tap", "Касание двумя пальцами"),
("Right Mouse", "Правая мышь"),
("One-Finger Move", "Движение одним пальцем"),
@@ -255,7 +259,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Do you accept?", "Вы согласны?"),
("Open System Setting", "Открыть настройки системы"),
("How to get Android input permission?", "Как получить разрешение на ввод Android?"),
- ("android_input_permission_tip1", "Чтобы удаленное устройство могло управлять вашим Android-устройством с помощью мыши или касания, вам необходимо разрешить RustDesk использовать службу «Специальные возможности»."),
+ ("android_input_permission_tip1", "Чтобы удаленное устройство могло управлять вашим Android-устройством с помощью мыши или касания, вам необходимо разрешить RustDesk использовать службу \"Специальные возможности\"."),
("android_input_permission_tip2", "Перейдите на следующую страницу системных настроек, найдите и войдите в [Установленные службы], включите службу [RustDesk Input]."),
("android_new_connection_tip", "Получен новый запрос на управление вашим текущим устройством."),
("android_service_will_start_tip", "Включение захвата экрана автоматически запускает службу, позволяя другим устройствам запрашивать соединение с этого устройства."),
@@ -263,21 +267,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_version_audio_tip", "Текущая версия Android не поддерживает захват звука, обновите ее до Android 10 или выше."),
("android_start_service_tip", "Нажмите [Запуск промежуточного сервера] или ОТКРЫТЬ разрешение [Захват экрана], чтобы запустить службу демонстрации экрана."),
("Account", "Аккаунт"),
- ("Quit", "Выйти"),
("Overwrite", "Перезаписать"),
- ("This file exists, skip or overwrite this file?", "Этот файл существует, пропустить или перезаписать этот файл?"),
+ ("This file exists, skip or overwrite this file?", "Этот файл существует, пропустить или перезаписать файл?"),
+ ("Quit", "Выйти"),
("doc_mac_permission", "https://rustdesk.com/docs/ru/manual/mac/#включение-разрешений"),
("Help", "Помощь"),
- ("Failed", "Неуспешный"),
+ ("Failed", "Не удалось"),
("Succeeded", "Успешно"),
- ("Someone turns on privacy mode, exit", "Кто-то включает режим конфиденциальности, выйдите"),
+ ("Someone turns on privacy mode, exit", "Кто-то включает режим конфиденциальности, выход"),
("Unsupported", "Не поддерживается"),
- ("Peer denied", "Отказано в пире"),
+ ("Peer denied", "Отклонено удалённым компьютером"),
("Please install plugins", "Пожалуйста, установите плагины"),
- ("Peer exit", "Одноранговый выход"),
- ("Failed to turn off", "Не удалось отключить"),
+ ("Peer exit", "Отключен удалённым компьютером"),
+ ("Failed to turn off", "Не удалось выключить"),
("Turned off", "Выключен"),
("In privacy mode", "В режиме конфиденциальности"),
("Out privacy mode", "Выход из режима конфиденциальности"),
+ ("Language", "Язык"),
+ ("Keep RustDesk background service", "Сохранить фоновый службу RustDesk"),
+ ("Ignore Battery Optimizations", "Игнорировать оптимизацию батареи"),
+ ("android_open_battery_optimizations_tip", "Перейдите на следующую страницу настроек "),
+ ("Connection not allowed", "Подключение не разрешено"),
+ ("Use temporary password", "Использовать временный пароль"),
+ ("Use permanent password", "Использовать постоянный пароль"),
+ ("Use both passwords", "Использовать оба пароля"),
+ ("Set permanent password", "Установить постоянный пароль"),
+ ("Set temporary password length", "Длина временного пароля"),
+ ("Enable Remote Restart", "Включить удаленный перезапуск"),
+ ("Allow remote restart", "Разрешить удаленный перезапуск"),
+ ("Restart Remote Device", "Перезапустить удаленное устройство"),
+ ("Are you sure you want to restart", "Вы уверены, что хотите выполнить перезапуск?"),
+ ("Restarting Remote Device", "Перезагрузка удаленного устройства"),
+ ("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."),
].iter().cloned().collect();
}
diff --git a/src/lang/sk.rs b/src/lang/sk.rs
index 6cea1479b..7f07657eb 100644
--- a/src/lang/sk.rs
+++ b/src/lang/sk.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "O RustDesk"),
("Mute", "Stíšiť"),
("Audio Input", "Zvukový vstup"),
+ ("Enhancements", ""),
+ ("Hardware Codec", ""),
+ ("Adaptive Bitrate", ""),
("ID Server", "ID server"),
("Relay Server", "Prepojovací server"),
("API Server", "API server"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimalizované pre čas odozvy"),
("Custom", "Vlastné"),
("Show remote cursor", "Zobrazovať vzdialený ukazovateľ myši"),
+ ("Show quality monitor", ""),
("Disable clipboard", "Vypnúť schránku"),
("Lock after session end", "Po skončení uzamknúť plochu"),
("Insert", "Vložiť"),
@@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Vypnutý"),
("In privacy mode", "V režime súkromia"),
("Out privacy mode", "Mimo režimu súkromia"),
+ ("Language", ""),
+ ("Keep RustDesk background service", ""),
+ ("Ignore Battery Optimizations", ""),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", ""),
+ ("Use temporary password", ""),
+ ("Use permanent password", ""),
+ ("Use both passwords", ""),
+ ("Set permanent password", ""),
+ ("Set temporary password length", ""),
+ ("Enable Remote Restart", ""),
+ ("Allow remote restart", ""),
+ ("Restart Remote Device", ""),
+ ("Are you sure you want to restart", ""),
+ ("Restarting Remote Device", ""),
+ ("remote_restarting_tip", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/template.rs b/src/lang/template.rs
index 91c572e44..e6b2bd01d 100644
--- a/src/lang/template.rs
+++ b/src/lang/template.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", ""),
("Mute", ""),
("Audio Input", ""),
+ ("Enhancements", ""),
+ ("Hardware Codec", ""),
+ ("Adaptive Bitrate", ""),
("ID Server", ""),
("Relay Server", ""),
("API Server", ""),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", ""),
("Custom", ""),
("Show remote cursor", ""),
+ ("Show quality monitor", ""),
("Disable clipboard", ""),
("Lock after session end", ""),
("Insert", ""),
@@ -264,7 +268,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_start_service_tip", ""),
("Account", ""),
("Overwrite", ""),
- ("This file exists, skip or overwrite this file?", "")
+ ("This file exists, skip or overwrite this file?", ""),
("Quit", ""),
("doc_mac_permission", ""),
("Help", ""),
@@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", ""),
("In privacy mode", ""),
("Out privacy mode", ""),
+ ("Language", ""),
+ ("Keep RustDesk background service", ""),
+ ("Ignore Battery Optimizations", ""),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", ""),
+ ("Use temporary password", ""),
+ ("Use permanent password", ""),
+ ("Use both passwords", ""),
+ ("Set permanent password", ""),
+ ("Set temporary password length", ""),
+ ("Enable Remote Restart", ""),
+ ("Allow remote restart", ""),
+ ("Restart Remote Device", ""),
+ ("Are you sure you want to restart", ""),
+ ("Restarting Remote Device", ""),
+ ("remote_restarting_tip", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/tr.rs b/src/lang/tr.rs
index d82901298..b06c0e7f3 100644
--- a/src/lang/tr.rs
+++ b/src/lang/tr.rs
@@ -11,12 +11,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable Service", "Servisi aktif et"),
("Start Service", "Servisi başlat"),
("Service is running", "Servis çalışıyor"),
- ("Service is not running", "Servis durduruldu"),
+ ("Service is not running", "Servis çalışmıyor"),
("not_ready_status", "Hazır değil. Bağlantınızı kontrol edin"),
("Control Remote Desktop", "Bağlanılacak Uzak Bağlantı ID"),
("Transfer File", "Dosya transferi"),
("Connect", "Bağlan"),
- ("Recent Sessions", "Sıklıkla Bağlanılanlar"),
+ ("Recent Sessions", "Son Bağlanılanlar"),
("Address Book", "Adres Defteri"),
("Confirmation", "Onayla"),
("TCP Tunneling", "TCP Tünelleri"),
@@ -33,8 +33,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Change ID", "ID Değiştir"),
("Website", "Website"),
("About", "Hakkında"),
- ("Mute", "Sesi Kapat"),
+ ("Mute", "Sustur"),
("Audio Input", "Ses Girişi"),
+ ("Enhancements", ""),
+ ("Hardware Codec", ""),
+ ("Adaptive Bitrate", ""),
("ID Server", "ID Sunucu"),
("Relay Server", "Relay Sunucu"),
("API Server", "API Sunucu"),
@@ -98,19 +101,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Unblock user input", "Kullanı girişine izin ver"),
("Adjust Window", "Pencereyi Ayarla"),
("Original", "Orjinal"),
- ("Shrink", "Sığdır"),
+ ("Shrink", "Küçült"),
("Stretch", "Uzat"),
("Good image quality", "İyi görüntü kalitesi"),
("Balanced", "Dengelenmiş"),
("Optimize reaction time", "Tepki süresini optimize et"),
("Custom", "Özel"),
("Show remote cursor", "Uzaktaki fare imlecini göster"),
+ ("Show quality monitor", ""),
("Disable clipboard", "Hafızadaki kopyalanmışları engelle"),
("Lock after session end", "Bağlantıdan sonra kilitle"),
("Insert", "Ekle"),
("Insert Lock", "Kilit Ekle"),
("Refresh", "Yenile"),
- ("ID does not exist", "ID hatalı"),
+ ("ID does not exist", "ID bulunamadı"),
("Failed to connect to rendezvous server", "ID oluşturma sunucusuna bağlanılamadı"),
("Please try later", "Dağa sonra tekrar deneyiniz"),
("Remote desktop is offline", "Uzak masaüstü kapalı"),
@@ -184,7 +188,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Port", "Port"),
("Settings", "Ayarlar"),
("Username", "Kullanıcı Adı"),
- ("Invalid port", "Geçersiz bağlantı noktası"),
+ ("Invalid port", "Geçersiz port"),
("Closed manually by the peer", "Eş tarafından manuel olarak kapatıldı"),
("Enable remote configuration modification", "Uzaktan yapılandırma değişikliğini etkinleştir"),
("Run without install", "Yüklemeden çalıştır"),
@@ -200,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Add ID", "ID Ekle"),
("Add Tag", "Etiket Ekle"),
("Unselect all tags", "Tüm etiketlerin seçimini kaldır"),
- ("Network error", "Network error"),
+ ("Network error", "Bağlantı hatası"),
("Username missed", "Kullanıcı adı boş"),
("Password missed", "Şifre boş"),
("Wrong credentials", "Yanlış kimlik bilgileri"),
@@ -270,14 +274,30 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Help", "Yardım"),
("Failed", "Arızalı"),
("Succeeded", "başarılı"),
- ("Someone turns on privacy mode, exit", "Birisi gizlilik modunu açar, çık"),
+ ("Someone turns on privacy mode, exit", "Birisi gizlilik modunu açarsa, çık"),
("Unsupported", "desteklenmiyor"),
- ("Peer denied", "akran reddedildi"),
+ ("Peer denied", "eş reddedildi"),
("Please install plugins", "Lütfen eklentileri yükleyin"),
- ("Peer exit", "akran çıkışı"),
+ ("Peer exit", "eş çıkışı"),
("Failed to turn off", "kapatılamadı"),
- ("Turned off", "Kapalı"),
+ ("Turned off", "Kapatıldı"),
("In privacy mode", "Gizlilik modunda"),
("Out privacy mode", "Gizlilik modu dışında"),
+ ("Language", "Dil"),
+ ("Keep RustDesk background service", ""),
+ ("Ignore Battery Optimizations", ""),
+ ("android_open_battery_optimizations_tip", ""),
+ ("Connection not allowed", "bağlantıya izin verilmedi"),
+ ("Use temporary password", "Geçici şifre kullan"),
+ ("Use permanent password", "Kalıcı şifre kullan"),
+ ("Use both passwords", "İki şifreyide kullan"),
+ ("Set permanent password", "Kalıcı şifre oluştur"),
+ ("Set temporary password length", ""),
+ ("Enable Remote Restart", "Uzaktan yeniden başlatmayı aktif et"),
+ ("Allow remote restart", "Uzaktan yeniden başlatmaya izin ver"),
+ ("Restart Remote Device", "Uzaktaki cihazı yeniden başlat"),
+ ("Are you sure you want to restart", "Yeniden başlatmak istediğinize emin misin?"),
+ ("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"),
+ ("remote_restarting_tip", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/tw.rs b/src/lang/tw.rs
index 2de3f4414..ce7804a65 100644
--- a/src/lang/tw.rs
+++ b/src/lang/tw.rs
@@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "關於"),
("Mute", "靜音"),
("Audio Input", "音訊輸入"),
+ ("Enhancements", "增強功能"),
+ ("Hardware Codec", "硬件編解碼"),
+ ("Adaptive Bitrate", "自適應碼率"),
("ID Server", "ID 伺服器"),
("Relay Server", "轉送伺服器"),
("API Server", "API 伺服器"),
@@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "回應速度最佳化"),
("Custom", "自訂"),
("Show remote cursor", "顯示遠端游標"),
+ ("Show quality monitor", "顯示質量監測"),
("Disable clipboard", "停用剪貼簿"),
("Lock after session end", "工作階段結束後鎖定電腦"),
("Insert", "插入"),
@@ -262,13 +266,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_stop_service_tip", "關閉服務將自動關閉所有已建立的連接。"),
("android_version_audio_tip", "目前的 Android 版本不支持音訊錄製,請升級至 Android 10 或以上版本。"),
("android_start_service_tip", "點擊 「啟動服務」 或啟用 「畫面錄製」 權限以開啟手機畫面共享服務。"),
- ("Account", "帳號"),
- ("Quit", "退出"),
+ ("Account", "帳戶"),
("Overwrite", "覆寫"),
("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要跳過或是覆寫此檔案嗎?"),
+ ("Quit", "退出"),
("doc_mac_permission", "https://rustdesk.com/docs/zh-tw/manual/mac/#啟用權限"),
("Help", "幫助"),
- ("Account", "帳戶"),
("Failed", "失敗"),
("Succeeded", "成功"),
("Someone turns on privacy mode, exit", "其他用戶開啟隱私模式,退出"),
@@ -280,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "退出"),
("In privacy mode", "開啟隱私模式"),
("Out privacy mode", "退出隱私模式"),
+ ("Language", "語言"),
+ ("Keep RustDesk background service", "保持RustDesk後台服務"),
+ ("Ignore Battery Optimizations", "忽略電池優化"),
+ ("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的RustDesk應用設置頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"),
+ ("Connection not allowed", "對方不允許連接"),
+ ("Use temporary password", "使用臨時密碼"),
+ ("Use permanent password", "使用固定密碼"),
+ ("Use both passwords", "同時使用兩種密碼"),
+ ("Set permanent password", "設定固定密碼"),
+ ("Set temporary password length", "設定臨時密碼長度"),
+ ("Enable Remote Restart", "允許遠程重啓"),
+ ("Allow remote restart", "允許遠程重啓"),
+ ("Restart Remote Device", "重啓遠程電腦"),
+ ("Are you sure you want to restart", "确定要重启"),
+ ("Restarting Remote Device", "正在重啓遠程設備"),
+ ("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"),
].iter().cloned().collect();
}
diff --git a/src/lang/vn.rs b/src/lang/vn.rs
new file mode 100644
index 000000000..014dcb20e
--- /dev/null
+++ b/src/lang/vn.rs
@@ -0,0 +1,303 @@
+lazy_static::lazy_static! {
+pub static ref T: std::collections::HashMap<&'static str, &'static str> =
+ [
+ ("Status", "Trạng thái hiện tại"),
+ ("Your Desktop", "Desktop của bạn"),
+ ("desk_tip", "Desktop của bạn có thể đuợc truy cập bằng ID và mật khẩu này."),
+ ("Password", "Mật khẩu"),
+ ("Ready", "Sẵn sàng"),
+ ("Established", "Đã đuợc thiết lập"),
+ ("connecting_status", "Đang kết nối đến mạng lưới RustDesk..."),
+ ("Enable Service", "Bật dịch vụ"),
+ ("Start Service", "Bắt đầu dịch vụ"),
+ ("Service is running", "Dịch vụ hiện đang chạy"),
+ ("Service is not running", "Dịch vụ hiện đang dừng"),
+ ("not_ready_status", "Hiện chưa sẵn sàng. Hãy kiểm tra kết nối của bạn"),
+ ("Control Remote Desktop", "Điều khiển Desktop Từ Xa"),
+ ("Transfer File", "Truyền Tệp Tin"),
+ ("Connect", "Kết nối"),
+ ("Recent Sessions", "Các session gần đây"),
+ ("Address Book", "Quyển địa chỉ"),
+ ("Confirmation", "Xác nhận"),
+ ("TCP Tunneling", "TCP Tunneling"),
+ ("Remove", "Loại bỏ"),
+ ("Refresh random password", "Làm mới mật khẩu ngẫu nhiên"),
+ ("Set your own password", "Đặt mật khẩu riêng"),
+ ("Enable Keyboard/Mouse", "Cho phép sử dụng bàn phím/chuột"),
+ ("Enable Clipboard", "Cho phép sử dụng clipboard"),
+ ("Enable File Transfer", "Cho phép truyền tệp tin"),
+ ("Enable TCP Tunneling", "Cho phép TCP Tunneling"),
+ ("IP Whitelisting", "Cho phép IP"),
+ ("ID/Relay Server", "Máy chủ ID/Relay"),
+ ("Stop service", "Dừng dịch vụ"),
+ ("Change ID", "Thay đổi ID"),
+ ("Website", "Trang web"),
+ ("About", "About"),
+ ("Mute", "Tắt tiếng"),
+ ("Audio Input", "Đầu vào âm thanh"),
+ ("Enhancements", "Các tiện itchs"),
+ ("Hardware Codec", "Codec phần cứng"),
+ ("Adaptive Bitrate", "Adaptive Bitrate"),
+ ("ID Server", "Máy chủ ID"),
+ ("Relay Server", "Máy chủ Relay"),
+ ("API Server", "Máy chủ API"),
+ ("invalid_http", "phải bắt đầu bằng http:// hoặc https://"),
+ ("Invalid IP", "IP không hợp lệ"),
+ ("id_change_tip", "Các kí tự đuợc phép là: từ a-z, A-Z, 0-9 và _ (dấu gạch dưới). Kí tự đầu tiên phải bắt đầu từ a-z, A-Z. Độ dài kí tự từ 6 đến 16"),
+ ("Invalid format", "Định dạng không hợp lệnh"),
+ ("server_not_support", "Chưa đuợc hỗ trợ bới server"),
+ ("Not available", "Chưa có mặt"),
+ ("Too frequent", "Quá thường xuyên"),
+ ("Cancel", "Hủy"),
+ ("Skip", "Bỏ qua"),
+ ("Close", "Đóng"),
+ ("Retry", "Thử lại"),
+ ("OK", "OK"),
+ ("Password Required", "Yêu cầu mật khẩu"),
+ ("Please enter your password", "Mời nhập mật khẩu"),
+ ("Remember password", "Nhớ mật khẩu"),
+ ("Wrong Password", "Sai mật khẩu"),
+ ("Do you want to enter again?", "Bạn có muốn nhập lại không"),
+ ("Connection Error", "Kết nối bị lỗi"),
+ ("Error", "Lỗi"),
+ ("Reset by the peer", "Đựoc cài đặt lại với peer"),
+ ("Connecting...", "Đang kết nối..."),
+ ("Connection in progress. Please wait.", "Đang kết nối. Xin chờ."),
+ ("Please try 1 minute later", "Hãy thử lại sau 1 phút"),
+ ("Login Error", "Đăng nhập bị lỗi"),
+ ("Successful", "Thành công"),
+ ("Connected, waiting for image...", "Đã kết nối, đang đợi hình ảnh..."),
+ ("Name", "Tên"),
+ ("Type", "Loại"),
+ ("Modified", "Chỉnh sửa"),
+ ("Size", "Kích cỡ"),
+ ("Show Hidden Files", "Hiển thị tệp tin bị ẩn"),
+ ("Receive", "Nhận"),
+ ("Send", "Gửi"),
+ ("Refresh File", "Làm mới tệp tin"),
+ ("Local", "Cục bộ"),
+ ("Remote", "Từ xa"),
+ ("Remote Computer", "Máy tính từ xa"),
+ ("Local Computer", "Máy tính cục bộ"),
+ ("Confirm Delete", "Xác nhận xóa"),
+ ("Delete", "Xóa"),
+ ("Properties", "Thuộc tính"),
+ ("Multi Select", "Chọn nhiều"),
+ ("Empty Directory", "Thư mục rỗng"),
+ ("Not an empty directory", "Không phải thư mục rỗng"),
+ ("Are you sure you want to delete this file?", "Bạn chắc bạn có muốn xóa tệp tin này không?"),
+ ("Are you sure you want to delete this empty directory?", "Bạn chắc bạn có muốn xóa thư mục rỗng này không?"),
+ ("Are you sure you want to delete the file of this directory?", "Bạn chắc bạn có muốn xóa những tệp tin trong thư mục này không?"),
+ ("Do this for all conflicts", "Xác nhận đối với tất cả các trùng lặp"),
+ ("This is irreversible!", "Không thể hoàn tác!"),
+ ("Deleting", "Đang xóa"),
+ ("files", "các tệp tin"),
+ ("Waiting", "Đang chờ"),
+ ("Finished", "Hoàn thành"),
+ ("Speed", "Tốc độ"),
+ ("Custom Image Quality", "Chất lượng hình ảnh"),
+ ("Privacy mode", "Chế độ riêng tư"),
+ ("Block user input", "Chặn các tương tác từ người dùng"),
+ ("Unblock user input", "Hủy chặn các tương tác từ người dùng"),
+ ("Adjust Window", "Điều chỉnh cửa sổ"),
+ ("Original", "Gốc"),
+ ("Shrink", "Thu nhỏ"),
+ ("Stretch", "Kéo dãn"),
+ ("Good image quality", "Chất lượng hình ảnh tốt"),
+ ("Balanced", "Cân bằng"),
+ ("Optimize reaction time", "Thời gian phản ứng tối ưu"),
+ ("Custom", "Custom"),
+ ("Show remote cursor", "Hiển thị con trỏ từ máy từ xa"),
+ ("Show quality monitor", "Hiện thị chất lượng của màn hình"),
+ ("Disable clipboard", "Tắt clipboard"),
+ ("Lock after session end", "Khóa sau khi kết thúc session"),
+ ("Insert", "Cài"),
+ ("Insert Lock", "Cài khóa"),
+ ("Refresh", "Làm mới"),
+ ("ID does not exist", "ID không tồn tại"),
+ ("Failed to connect to rendezvous server", "Không thể kết nối đến máy chủ rendezvous"),
+ ("Please try later", "Thử lại sau"),
+ ("Remote desktop is offline", "Máy tính từ xa hiện đang offline"),
+ ("Key mismatch", "Chìa không khớp"),
+ ("Timeout", "Quá thời gian"),
+ ("Failed to connect to relay server", "Không thể kết nối tới máy chủ relay"),
+ ("Failed to connect via rendezvous server", "Không thể kết nối qua máy chủ rendezvous"),
+ ("Failed to connect via relay server", "Không thể kết nối qua máy chủ relay"),
+ ("Failed to make direct connection to remote desktop", "Không thể kết nối thẳng tới máy tính từ xa"),
+ ("Set Password", "Cài đặt mật khẩu"),
+ ("OS Password", "Mật khẩu hệ điều hành"),
+ ("install_tip", "Do UAC, RustDesk sẽ không thể hoạt động đúng cách là bên từ xa trong vài trường hợp. Để tránh UAC, hãy nhấn cái nút dưới đây để cài RustDesk vào hệ thống."),
+ ("Click to upgrade", "Nhấn để nâng cấp"),
+ ("Click to download", "Nhấn để tải xuống"),
+ ("Click to update", "Nhấn để cập nhật"),
+ ("Configure", "Cài đặt"),
+ ("config_acc", "Để có thể điều khiển máy tính từ xa, bạn cần phải cung cấp quyền \"Trợ năng\" cho RustDesk"),
+ ("config_screen", "Để có thể truy cập máy tính từ xa, bạn cần phải cung cấp quyền \"Ghi Màn Hình\" cho RustDesk."),
+ ("Installing ...", "Đang cài ..."),
+ ("Install", "Cài"),
+ ("Installation", "Cài"),
+ ("Installation Path", "Địa điểm cài"),
+ ("Create start menu shortcuts", "Tạo shortcut tại start menu"),
+ ("Create desktop icon", "Tạo biểu tượng trên desktop"),
+ ("agreement_tip", "Bằng cách bắt đầu cài đặt, bạn chấp nhận thỏa thuận cấp phép."),
+ ("Accept and Install", "Chấp nhận và cài"),
+ ("End-user license agreement", "Thỏa thuận cấp phép dành cho người dùng"),
+ ("Generating ...", "Đang tạo ..."),
+ ("Your installation is lower version.", "Phiên bản của bạn là phiên bản cũ"),
+ ("not_close_tcp_tip", "Đừng đóng cửa sổ này khi bạn đang sử dụng tunnel"),
+ ("Listening ...", "Đang nghe ..."),
+ ("Remote Host", "Máy từ xa"),
+ ("Remote Port", "Cổng từ xa"),
+ ("Action", "Hành động"),
+ ("Add", "Thêm"),
+ ("Local Port", "Cổng nội bộ"),
+ ("setup_server_tip", "Để kết nối nhanh hơn, hãy tự tạo máy chủ riêng"),
+ ("Too short, at least 6 characters.", "Quá ngắn, độ dài phải ít nhất là 6."),
+ ("The confirmation is not identical.", "Xác minh không khớp"),
+ ("Permissions", "Quyền"),
+ ("Accept", "Chấp nhận"),
+ ("Dismiss", "Bỏ qua"),
+ ("Disconnect", "Ngắt kết nối"),
+ ("Allow using keyboard and mouse", "Cho phép sử dụng bàn phím và chuột"),
+ ("Allow using clipboard", "Cho phép sử dụng clipboard"),
+ ("Allow hearing sound", "Cho phép nghe âm thanh"),
+ ("Allow file copy and paste", "Cho phép sao chép và dán tệp tin"),
+ ("Connected", "Đã kết nối"),
+ ("Direct and encrypted connection", "Kết nối trực tiếp và đuợc mã hóa"),
+ ("Relayed and encrypted connection", "Kết nối relay và đuợc mã hóa"),
+ ("Direct and unencrypted connection", "Kết nối trực tiếp và không đuợc mã hóa"),
+ ("Relayed and unencrypted connection", "Kết nối relay và không đuợc mã hóa"),
+ ("Enter Remote ID", "Nhập ID từ xa"),
+ ("Enter your password", "Nhập mật khẩu"),
+ ("Logging in...", "Đang đăng nhập"),
+ ("Enable RDP session sharing", "Cho phép chia sẻ session RDP"),
+ ("Auto Login", "Tự động đăng nhập"),
+ ("Enable Direct IP Access", "Cho phép truy cập trực tiếp qua IP"),
+ ("Rename", "Đổi tên"),
+ ("Space", "Space"),
+ ("Create Desktop Shortcut", "Tạo shortcut trên desktop"),
+ ("Change Path", "Đổi địa điểm"),
+ ("Create Folder", "Tạo thư mục"),
+ ("Please enter the folder name", "Hãy nhập tên thư mục"),
+ ("Fix it", "Sửa nó"),
+ ("Warning", "Cảnh báo"),
+ ("Login screen using Wayland is not supported", "Màn hình đăng nhập sử dụng Wayland không đựoc hỗ trợ"),
+ ("Reboot required", "Yêu cầu khởi động lại"),
+ ("Unsupported display server ", "Máy chủ hiển thị không đuợc hỗ trọ"),
+ ("x11 expected", "Cần x11"),
+ ("Port", "Cổng"),
+ ("Settings", "Cài đặt"),
+ ("Username", "Tên người dùng"),
+ ("Invalid port", "Cổng không hợp lệ"),
+ ("Closed manually by the peer", "Đóng thủ công bởi peer"),
+ ("Enable remote configuration modification", "Cho phép thay đổi cấu hình bên từ xa"),
+ ("Run without install", "Chạy mà không cần cài"),
+ ("Always connected via relay", "Luôn đuợc kết nối qua relay"),
+ ("Always connect via relay", "Luôn kết nối qua relay"),
+ ("whitelist_tip", "Chỉ có những IP đựoc cho phép mới có thể truy cập"),
+ ("Login", "Đăng nhập"),
+ ("Logout", "Đăng xuất"),
+ ("Tags", "Tags"),
+ ("Search ID", "Tìm ID"),
+ ("Current Wayland display server is not supported", "Máy chủ hình ảnh Wayland hiện không đuợc hỗ trợ"),
+ ("whitelist_sep", "Đuợc cách nhau bởi dấu phẩy, dấu chấm phẩy, dấu cách hay dòng mới"),
+ ("Add ID", "Thêm ID"),
+ ("Add Tag", "Thêm Tag"),
+ ("Unselect all tags", "Hủy chọn tất cả các tag"),
+ ("Network error", "Lỗi mạng"),
+ ("Username missed", "Mất tên người dùng"),
+ ("Password missed", "Mất mật khẩu"),
+ ("Wrong credentials", "Chứng danh bị sai"),
+ ("Edit Tag", "Chỉnh sửa Tag"),
+ ("Unremember Password", "Quên mật khẩu"),
+ ("Favorites", "Favorites"),
+ ("Add to Favorites", "Thêm vào mục Favorites"),
+ ("Remove from Favorites", "Xóa khỏi mục Favorites"),
+ ("Empty", "Trống"),
+ ("Invalid folder name", "Tên thư mục không hợp lệ"),
+ ("Socks5 Proxy", "Proxy Socks5"),
+ ("Hostname", "Tên host"),
+ ("Discovered", "Đuợc phát hiện"),
+ ("install_daemon_tip", "Để chạy lúc khởi động máy, bạn cần phải cài dịch vụ hệ thống."),
+ ("Remote ID", "ID từ xa"),
+ ("Paste", "Dán"),
+ ("Paste here?", "Dán ở đây?"),
+ ("Are you sure to close the connection?", "Bạn có chắc muốn đóng kết nối không"),
+ ("Download new version", "Tải về phiên bản mới"),
+ ("Touch mode", "Chế độ chạm"),
+ ("Mouse mode", "Chế độ dùng chuột"),
+ ("One-Finger Tap", "Chạm bằng một ngón tay"),
+ ("Left Mouse", "Chuột trái"),
+ ("One-Long Tap", "Chạm lâu bằng một ngón tay"),
+ ("Two-Finger Tap", "Chạm bằng hai ngón tay"),
+ ("Right Mouse", "Chuột phải"),
+ ("One-Finger Move", "Di chuyển bằng một ngón tay"),
+ ("Double Tap & Move", "Chạm hai lần và di chuyển"),
+ ("Mouse Drag", "Di chuyển bằng chuột"),
+ ("Three-Finger vertically", "Ba ngón tay theo chiều dọc"),
+ ("Mouse Wheel", "Bánh xe lăn trê con chuột"),
+ ("Two-Finger Move", "Di chuyển bằng hai ngón tay"),
+ ("Canvas Move", "Di chuyển canvas"),
+ ("Pinch to Zoom", "Véo để phóng to/nhỏ"),
+ ("Canvas Zoom", "Phóng to/nhỏ canvas"),
+ ("Reset canvas", "Cài đặt lại canvas"),
+ ("No permission of file transfer", "Không có quyền truyền tệp tin"),
+ ("Note", "Ghi nhớ"),
+ ("Connection", "Kết nối"),
+ ("Share Screen", "Chia sẻ màn hình"),
+ ("CLOSE", "ĐÓNG"),
+ ("OPEN", "MỞ"),
+ ("Chat", "Chat"),
+ ("Total", "Tổng"),
+ ("items", "items"),
+ ("Selected", "Đã đuợc chọn"),
+ ("Screen Capture", "Ghi màn hình"),
+ ("Input Control", "Điều khiển đầu vào"),
+ ("Audio Capture", "Ghi âm thanh"),
+ ("File Connection", "Kết nối tệp tin"),
+ ("Screen Connection", "Kết nối màn hình"),
+ ("Do you accept?", "Bạn có chấp nhận không?"),
+ ("Open System Setting", "Mở cài đặt hệ thống"),
+ ("How to get Android input permission?", "Cách để có quyền nhập trên Android?"),
+ ("android_input_permission_tip1", "Để thiết bị từ xa điều khiển thiết bị Android của bạn bằng chuột hoặc chạm, bạn cần cho phép RustDesk sử dụng dịch vụ \"Trợ năng\"."),
+ ("android_input_permission_tip2", "Vui lòng chuyển đến trang cài đặt hệ thống tiếp theo, tìm và nhập [Dịch vụ đã cài đặt], bật dịch vụ [RustDesk Input]."),
+ ("android_new_connection_tip", "Yêu cầu kiểm soát mới đã được nhận, yêu cầu này muốn kiểm soát thiết bị hiện tại của bạn."),
+ ("android_service_will_start_tip", "Bật \"Ghi màn hình\" sẽ tự động khởi động dịch vụ, cho phép các thiết bị khác yêu cầu kết nối với thiết bị của bạn."),
+ ("android_stop_service_tip", "Đóng dịch vụ sẽ tự động đóng tất cả các kết nối đã thiết lập."),
+ ("android_version_audio_tip", "Phiên bản Android hiện tại không hỗ trợ ghi âm, vui lòng nâng cấp lên Android 10 trở lên."),
+ ("android_start_service_tip", "Nhấn vào [Bắt đầu dịch vụ] hoặc MỞ quyền [Ghi màn hình] để bắt đầu dịch vụ chia sẻ màn hình."),
+ ("Account", "Tài khoản"),
+ ("Overwrite", "Ghi đè"),
+ ("This file exists, skip or overwrite this file?", "Tệp tin này đã tồn tại, bạn có muốn bỏ qua hay ghi đè lên tệp tin này?"),
+ ("Quit", "Thoát"),
+ ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"),
+ ("Help", "Giúp đỡ"),
+ ("Failed", "Thất bại"),
+ ("Succeeded", "Thành công"),
+ ("Someone turns on privacy mode, exit", "Ai đó đã bật chế độ riêng tư, thoát"),
+ ("Unsupported", "Không hỗ trợ"),
+ ("Peer denied", "Peer đã từ chối"),
+ ("Please install plugins", "Hãy cài plugins"),
+ ("Peer exit", "Peer đã thoát"),
+ ("Failed to turn off", "Không thể tắt"),
+ ("Turned off", "Đã tắt"),
+ ("In privacy mode", "Vào chế độ riêng tư"),
+ ("Out privacy mode", "Thoát chế độ riêng tư"),
+ ("Language", "Ngôn ngữ"),
+ ("Keep RustDesk background service", "Giữ dịch vụ nền RustDesk"),
+ ("Ignore Battery Optimizations", "Bỏ qua các tối ưu pin"),
+ ("android_open_battery_optimizations_tip", "Nếu bạn muốn tắt tính năng này, vui lòng chuyển đến trang cài đặt ứng dụng RustDesk tiếp theo, tìm và nhập [Pin], Bỏ chọn [Không hạn chế]"),
+ ("Connection not allowed", "Kết nối không đuợc phép"),
+ ("Use temporary password", "Sử dụng mật khẩu tạm thời"),
+ ("Use permanent password", "Sử dụng mật khẩu vĩnh viễn"),
+ ("Use both passwords", "Sử dụng cả hai mật khẩu"),
+ ("Set permanent password", "Đặt mật khẩu vĩnh viễn"),
+ ("Set temporary password length", "Đặt chiều dài của mật khẩu tạm thời"),
+ ("Enable Remote Restart", "Bật khởi động lại từ xa"),
+ ("Allow remote restart", "Cho phép khởi động lại từ xa"),
+ ("Restart Remote Device", "Khởi động lại thiết bị từ xa"),
+ ("Are you sure you want to restart", "Bạn có chắc bạn muốn khởi động lại không"),
+ ("Restarting Remote Device", "Đang khởi động lại thiết bị từ xa"),
+ ("remote_restarting_tip", "Thiết bị từ xa đang khởi động lại, hãy đóng cửa sổ tin nhắn này và kết nối lại với mật khẩu vĩnh viễn sau một khoảng thời gian"),
+ ].iter().cloned().collect();
+}
diff --git a/src/lib.rs b/src/lib.rs
index 6aa186d96..b7d1883c8 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -12,6 +12,8 @@ mod client;
#[cfg(not(any(target_os = "ios")))]
mod rendezvous_mediator;
#[cfg(not(any(target_os = "ios")))]
+mod lan;
+#[cfg(not(any(target_os = "ios")))]
pub use self::rendezvous_mediator::*;
/// cbindgen:ignore
pub mod common;
diff --git a/src/main.rs b/src/main.rs
index 6aee5cb89..8ecc72ad6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -108,6 +108,10 @@ fn main() {
args.len() > 1,
));
return;
+ } else if args[0] == "--extract" {
+ #[cfg(feature = "with_rc")]
+ hbb_common::allow_err!(crate::rc::extract_resources(&args[1]));
+ return;
}
}
if args[0] == "--remove" {
@@ -148,9 +152,13 @@ fn main() {
return;
} else if args[0] == "--password" {
if args.len() == 2 {
- ipc::set_password(args[1].to_owned()).unwrap();
+ ipc::set_permanent_password(args[1].to_owned()).unwrap();
}
return;
+ } else if args[0] == "--check-hwcodec-config" {
+ #[cfg(feature = "hwcodec")]
+ scrap::hwcodec::check_config();
+ return;
}
}
ui::start(&mut args[..]);
@@ -164,18 +172,18 @@ fn import_config(path: &str) {
let path = std::path::Path::new(path);
log::info!("import config from {:?} and {:?}", path, path2);
let config: Config = load_path(path.into());
- if config.id.is_empty() || config.key_pair.0.is_empty() {
+ if config.is_empty() {
log::info!("Empty source config, skipped");
return;
}
if get_modified_time(&path) > get_modified_time(&Config::file()) {
- if Config::set(config) {
+ if store_path(Config::file(), config).is_err() {
log::info!("config written");
}
}
let config2: Config2 = load_path(path2.into());
if get_modified_time(&path2) > get_modified_time(&Config2::file()) {
- if Config2::set(config2) {
+ if store_path(Config2::file(), config2).is_err() {
log::info!("config2 written");
}
}
diff --git a/src/platform/linux.rs b/src/platform/linux.rs
index efd6476b6..85947a143 100644
--- a/src/platform/linux.rs
+++ b/src/platform/linux.rs
@@ -1,4 +1,5 @@
use super::{CursorData, ResultType};
+pub use hbb_common::platform::linux::*;
use hbb_common::{allow_err, bail, log};
use libc::{c_char, c_int, c_void};
use std::{
@@ -8,6 +9,7 @@ use std::{
Arc,
},
};
+
type Xdo = *const c_void;
pub const PA_SAMPLE_RATE: u32 = 48000;
@@ -109,7 +111,8 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType {
cd.id = (*img).cursor_serial as _;
let pixels =
std::slice::from_raw_parts((*img).pixels, (cd.width * cd.height) as _);
- cd.colors.resize(pixels.len() * 4, 0);
+ // cd.colors.resize(pixels.len() * 4, 0);
+ let mut cd_colors = vec![0_u8; pixels.len() * 4];
for y in 0..cd.height {
for x in 0..cd.width {
let pos = (y * cd.width + x) as usize;
@@ -122,12 +125,13 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType {
continue;
}
let pos = pos * 4;
- cd.colors[pos] = r as _;
- cd.colors[pos + 1] = g as _;
- cd.colors[pos + 2] = b as _;
- cd.colors[pos + 3] = a as _;
+ cd_colors[pos] = r as _;
+ cd_colors[pos + 1] = g as _;
+ cd_colors[pos + 2] = b as _;
+ cd_colors[pos + 3] = a as _;
}
}
+ cd.colors = cd_colors.into();
res = Some(cd);
}
if !img.is_null() {
@@ -143,7 +147,75 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType {
}
}
+fn start_uinput_service() {
+ use crate::server::uinput::service;
+ std::thread::spawn(|| {
+ service::start_service_control();
+ });
+ std::thread::spawn(|| {
+ service::start_service_keyboard();
+ });
+ std::thread::spawn(|| {
+ service::start_service_mouse();
+ });
+}
+
+fn try_start_user_service(username: &str) {
+ if username == "" || username == "root" {
+ return;
+ }
+
+ if let Ok(mut cur_username) =
+ run_cmds("ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1".to_owned())
+ {
+ cur_username = cur_username.trim().to_owned();
+ if cur_username != "root" && cur_username != username {
+ let _ = run_cmds(format!(
+ "systemctl --machine={}@.host --user stop rustdesk",
+ &cur_username
+ ));
+ } else if cur_username == username {
+ return;
+ }
+ }
+
+ let _ = run_cmds(format!(
+ "systemctl --machine={}@.host --user start rustdesk",
+ username
+ ));
+}
+
+fn try_stop_user_service() {
+ if let Ok(mut username) =
+ run_cmds("ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1".to_owned())
+ {
+ username = username.trim().to_owned();
+ if username != "root" {
+ let _ = run_cmds(format!(
+ "systemctl --machine={}@.host --user stop rustdesk",
+ &username
+ ));
+ }
+ }
+}
+
+fn stop_server(server: &mut Option) {
+ if let Some(mut ps) = server.take() {
+ allow_err!(ps.kill());
+ std::thread::sleep(std::time::Duration::from_millis(30));
+ match ps.try_wait() {
+ Ok(Some(_status)) => {}
+ Ok(None) => {
+ let _res = ps.wait();
+ }
+ Err(e) => log::error!("error attempting to wait: {e}"),
+ }
+ }
+}
+
pub fn start_os_service() {
+ start_uinput_service();
+
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let mut uid = "".to_owned();
@@ -157,85 +229,106 @@ pub fn start_os_service() {
let mut cm0 = false;
let mut last_restart = std::time::Instant::now();
while running.load(Ordering::SeqCst) {
- let cm = get_cm();
- let tmp = get_active_userid();
- let mut start_new = false;
- if tmp != uid && !tmp.is_empty() {
- uid = tmp;
- log::info!("uid of seat0: {}", uid);
- let gdm = format!("/run/user/{}/gdm/Xauthority", uid);
- let mut auth = get_env_tries("XAUTHORITY", &uid, 10);
- if auth.is_empty() {
- auth = if std::path::Path::new(&gdm).exists() {
- gdm
- } else {
- let username = get_active_username();
- if username == "root" {
- format!("/{}/.Xauthority", username)
+ let username = get_active_username();
+ let is_wayland = current_is_wayland();
+
+ if username == "root" || !is_wayland {
+ // try stop user service
+ try_stop_user_service();
+
+ // try start subprocess "--server"
+ let cm = get_cm();
+ let tmp = get_active_userid();
+ let mut start_new = false;
+ if tmp != uid && !tmp.is_empty() {
+ uid = tmp;
+ log::info!("uid of seat0: {}", uid);
+ let gdm = format!("/run/user/{}/gdm/Xauthority", uid);
+ let mut auth = get_env_tries("XAUTHORITY", &uid, 10);
+ if auth.is_empty() {
+ auth = if std::path::Path::new(&gdm).exists() {
+ gdm
} else {
- let tmp = format!("/home/{}/.Xauthority", username);
- if std::path::Path::new(&tmp).exists() {
- tmp
+ let username = get_active_username();
+ if username == "root" {
+ format!("/{}/.Xauthority", username)
} else {
- format!("/var/lib/{}/.Xauthority", username)
+ let tmp = format!("/home/{}/.Xauthority", username);
+ if std::path::Path::new(&tmp).exists() {
+ tmp
+ } else {
+ format!("/var/lib/{}/.Xauthority", username)
+ }
}
- }
- };
- }
- let mut d = get_env("DISPLAY", &uid);
- if d.is_empty() {
- d = get_display();
- }
- if d.is_empty() {
- d = ":0".to_owned();
- }
- d = d.replace(&whoami::hostname(), "").replace("localhost", "");
- log::info!("DISPLAY: {}", d);
- log::info!("XAUTHORITY: {}", auth);
- std::env::set_var("XAUTHORITY", auth);
- std::env::set_var("DISPLAY", d);
- if let Some(ps) = server.as_mut() {
- allow_err!(ps.kill());
- std::thread::sleep(std::time::Duration::from_millis(30));
- last_restart = std::time::Instant::now();
- }
- } else if !cm
- && ((cm0 && last_restart.elapsed().as_secs() > 60)
- || last_restart.elapsed().as_secs() > 3600)
- {
- // restart server if new connections all closed, or every one hour,
- // as a workaround to resolve "SpotUdp" (dns resolve)
- // and x server get displays failure issue
- if let Some(ps) = server.as_mut() {
- allow_err!(ps.kill());
- std::thread::sleep(std::time::Duration::from_millis(30));
- last_restart = std::time::Instant::now();
- log::info!("restart server");
- }
- }
- if let Some(ps) = server.as_mut() {
- match ps.try_wait() {
- Ok(Some(_)) => {
- server = None;
- start_new = true;
+ };
}
- _ => {}
+ let mut d = get_env("DISPLAY", &uid);
+ if d.is_empty() {
+ d = get_display();
+ }
+ if d.is_empty() {
+ d = ":0".to_owned();
+ }
+ d = d.replace(&whoami::hostname(), "").replace("localhost", "");
+ log::info!("DISPLAY: {}", d);
+ log::info!("XAUTHORITY: {}", auth);
+ std::env::set_var("XAUTHORITY", auth);
+ std::env::set_var("DISPLAY", d);
+ if let Some(ps) = server.as_mut() {
+ allow_err!(ps.kill());
+ std::thread::sleep(std::time::Duration::from_millis(30));
+ last_restart = std::time::Instant::now();
+ }
+ } else if !cm
+ && ((cm0 && last_restart.elapsed().as_secs() > 60)
+ || last_restart.elapsed().as_secs() > 3600)
+ {
+ // restart server if new connections all closed, or every one hour,
+ // as a workaround to resolve "SpotUdp" (dns resolve)
+ // and x server get displays failure issue
+ if let Some(ps) = server.as_mut() {
+ allow_err!(ps.kill());
+ std::thread::sleep(std::time::Duration::from_millis(30));
+ last_restart = std::time::Instant::now();
+ log::info!("restart server");
+ }
+ }
+ if let Some(ps) = server.as_mut() {
+ match ps.try_wait() {
+ Ok(Some(_)) => {
+ server = None;
+ start_new = true;
+ }
+ _ => {}
+ }
+ } else {
+ start_new = true;
+ }
+ if start_new {
+ match crate::run_me(vec!["--server"]) {
+ Ok(ps) => server = Some(ps),
+ Err(err) => {
+ log::error!("Failed to start server: {}", err);
+ }
+ }
+ }
+ cm0 = cm;
+ } else if username != "" {
+ if username != "gdm" {
+ // try kill subprocess "--server"
+ stop_server(&mut server);
+
+ // try start user service
+ try_start_user_service(&username);
}
} else {
- start_new = true;
+ try_stop_user_service();
+ stop_server(&mut server);
}
- if start_new {
- match crate::run_me(vec!["--server"]) {
- Ok(ps) => server = Some(ps),
- Err(err) => {
- log::error!("Failed to start server: {}", err);
- }
- }
- }
- cm0 = cm;
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL));
}
+ try_stop_user_service();
if let Some(ps) = server.take().as_mut() {
allow_err!(ps.kill());
}
@@ -246,17 +339,6 @@ pub fn get_active_userid() -> String {
get_value_of_seat0(1)
}
-fn is_active(sid: &str) -> bool {
- if let Ok(output) = std::process::Command::new("loginctl")
- .args(vec!["show-session", "-p", "State", sid])
- .output()
- {
- String::from_utf8_lossy(&output.stdout).contains("active")
- } else {
- false
- }
-}
-
fn get_cm() -> bool {
if let Ok(output) = std::process::Command::new("ps").args(vec!["aux"]).output() {
for line in String::from_utf8_lossy(&output.stdout).lines() {
@@ -312,89 +394,6 @@ fn get_display() -> String {
last
}
-fn get_value_of_seat0(i: usize) -> String {
- if let Ok(output) = std::process::Command::new("loginctl").output() {
- for line in String::from_utf8_lossy(&output.stdout).lines() {
- if line.contains("seat0") {
- if let Some(sid) = line.split_whitespace().nth(0) {
- if is_active(sid) {
- if let Some(uid) = line.split_whitespace().nth(i) {
- return uid.to_owned();
- }
- }
- }
- }
- }
- }
-
- // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
- if let Ok(output) = std::process::Command::new("loginctl").output() {
- for line in String::from_utf8_lossy(&output.stdout).lines() {
- if let Some(sid) = line.split_whitespace().nth(0) {
- let d = get_display_server_of_session(sid);
- if is_active(sid) && d != "tty" {
- if let Some(uid) = line.split_whitespace().nth(i) {
- return uid.to_owned();
- }
- }
- }
- }
- }
-
- return "".to_owned();
-}
-
-pub fn get_display_server() -> String {
- let session = get_value_of_seat0(0);
- get_display_server_of_session(&session)
-}
-
-fn get_display_server_of_session(session: &str) -> String {
- if let Ok(output) = std::process::Command::new("loginctl")
- .args(vec!["show-session", "-p", "Type", session])
- .output()
- // Check session type of the session
- {
- let display_server = String::from_utf8_lossy(&output.stdout)
- .replace("Type=", "")
- .trim_end()
- .into();
- if display_server == "tty" {
- // If the type is tty...
- if let Ok(output) = std::process::Command::new("loginctl")
- .args(vec!["show-session", "-p", "TTY", session])
- .output()
- // Get the tty number
- {
- let tty: String = String::from_utf8_lossy(&output.stdout)
- .replace("TTY=", "")
- .trim_end()
- .into();
- if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty))
- // And check if Xorg is running on that tty
- {
- if xorg_results.trim_end().to_string() != "" {
- // If it is, manually return "x11", otherwise return tty
- "x11".to_owned()
- } else {
- display_server
- }
- } else {
- // If any of these commands fail just fall back to the display server
- display_server
- }
- } else {
- display_server
- }
- } else {
- // If the session is not a tty, then just return the type as usual
- display_server
- }
- } else {
- "".to_owned()
- }
-}
-
pub fn is_login_wayland() -> bool {
if let Ok(contents) = std::fs::read_to_string("/etc/gdm3/custom.conf") {
contents.contains("#WaylandEnable=false")
@@ -601,13 +600,6 @@ pub fn is_installed() -> bool {
true
}
-pub fn run_cmds(cmds: String) -> ResultType {
- let output = std::process::Command::new("sh")
- .args(vec!["-c", &cmds])
- .output()?;
- Ok(String::from_utf8_lossy(&output.stdout).to_string())
-}
-
fn get_env_tries(name: &str, uid: &str, n: usize) -> String {
for _ in 0..n {
let x = get_env(name, uid);
diff --git a/src/platform/macos.rs b/src/platform/macos.rs
index dabe11e49..1a8096587 100644
--- a/src/platform/macos.rs
+++ b/src/platform/macos.rs
@@ -342,7 +342,7 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType {
}
Ok(CursorData {
id: hcursor,
- colors,
+ colors: colors.into(),
hotx: hotspot.x as _,
hoty: hotspot.y as _,
width: size.width as _,
diff --git a/src/platform/windows.rs b/src/platform/windows.rs
index 4e8f7e16a..cb0fd778f 100644
--- a/src/platform/windows.rs
+++ b/src/platform/windows.rs
@@ -164,7 +164,7 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType {
Ok(CursorData {
id: hcursor,
- colors: cbits,
+ colors: cbits.into(),
hotx: ii.0.xHotspot as _,
hoty: ii.0.yHotspot as _,
width: width as _,
@@ -1020,6 +1020,22 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\"
// https://docs.microsoft.com/zh-cn/windows/win32/msi/uninstall-registry-key?redirectedfrom=MSDNa
// https://www.windowscentral.com/how-edit-registry-using-command-prompt-windows-10
// https://www.tenforums.com/tutorials/70903-add-remove-allowed-apps-through-windows-firewall-windows-10-a.html
+ // Note: without if exist, the bat may exit in advance on some Windows7 https://github.com/rustdesk/rustdesk/issues/895
+ let dels = format!(
+ "
+if exist \"{mk_shortcut}\" del /f /q \"{mk_shortcut}\"
+if exist \"{uninstall_shortcut}\" del /f /q \"{uninstall_shortcut}\"
+if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\"
+if exist \"{tmp_path}\\{app_name}.lnk\" del /f /q \"{tmp_path}\\{app_name}.lnk\"
+if exist \"{tmp_path}\\Uninstall {app_name}.lnk\" del /f /q \"{tmp_path}\\Uninstall {app_name}.lnk\"
+if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} Tray.lnk\"
+ ",
+ mk_shortcut = mk_shortcut,
+ uninstall_shortcut = uninstall_shortcut,
+ tray_shortcut = tray_shortcut,
+ tmp_path = tmp_path,
+ app_name = crate::get_app_name(),
+ );
let cmds = format!(
"
{uninstall_str}
@@ -1048,17 +1064,13 @@ cscript \"{tray_shortcut}\"
copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\"
{shortcuts}
copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
-del /f \"{mk_shortcut}\"
-del /f \"{uninstall_shortcut}\"
-del /f \"{tray_shortcut}\"
-del /f \"{tmp_path}\\{app_name}.lnk\"
-del /f \"{tmp_path}\\Uninstall {app_name}.lnk\"
-del /f \"{tmp_path}\\{app_name} Tray.lnk\"
+{dels}
sc create {app_name} binpath= \"\\\"{exe}\\\" --import-config \\\"{config_path}\\\"\" start= auto DisplayName= \"{app_name} Service\"
sc start {app_name}
sc stop {app_name}
sc delete {app_name}
{after_install}
+{sleep}
",
uninstall_str=uninstall_str,
path=path,
@@ -1081,6 +1093,16 @@ sc delete {app_name}
config_path=Config::file().to_str().unwrap_or(""),
lic=register_licence(),
after_install=get_after_install(&exe),
+ sleep=if debug {
+ "timeout 300"
+ } else {
+ ""
+ },
+ dels=if debug {
+ ""
+ } else {
+ &dels
+ },
);
run_cmds(cmds, debug, "install")?;
std::thread::sleep(std::time::Duration::from_millis(2000));
@@ -1126,10 +1148,10 @@ fn get_uninstall() -> String {
"
{before_uninstall}
reg delete {subkey} /f
- rd /s /q \"{path}\"
- rd /s /q \"{start_menu}\"
- del /f /q \"%PUBLIC%\\Desktop\\{app_name}*\"
- del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\"
+ if exist \"{path}\" rd /s /q \"{path}\"
+ if exist \"{start_menu}\" rd /s /q \"{start_menu}\"
+ if exist \"%PUBLIC%\\Desktop\\{app_name}.lnk\" del /f /q \"%PUBLIC%\\Desktop\\{app_name}.lnk\"
+ if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\"
",
before_uninstall=get_before_uninstall(),
subkey=subkey,
@@ -1176,11 +1198,8 @@ fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> {
.show(show)
.force_prompt(true)
.status();
- // leave the file for debug if execution failed
- if let Ok(res) = res {
- if res.success() {
- allow_err!(std::fs::remove_file(tmp));
- }
+ if !show {
+ allow_err!(std::fs::remove_file(tmp));
}
let _ = res?;
Ok(())
diff --git a/src/port_forward.rs b/src/port_forward.rs
index afc65f236..a17ee8259 100644
--- a/src/port_forward.rs
+++ b/src/port_forward.rs
@@ -42,6 +42,7 @@ fn run_rdp(port: u16) {
pub async fn listen(
id: String,
+ password: String,
port: i32,
interface: impl Interface,
ui_receiver: mpsc::UnboundedReceiver,
@@ -61,8 +62,9 @@ pub async fn listen(
Ok((forward, addr)) = listener.accept() => {
log::info!("new connection from {:?}", addr);
let id = id.clone();
+ let password = password.clone();
let mut forward = Framed::new(forward, BytesCodec::new());
- match connect_and_login(&id, &mut ui_receiver, interface.clone(), &mut forward, key, token, is_rdp).await {
+ match connect_and_login(&id, &password, &mut ui_receiver, interface.clone(), &mut forward, key, token, is_rdp).await {
Ok(Some(stream)) => {
let interface = interface.clone();
tokio::spawn(async move {
@@ -96,6 +98,7 @@ pub async fn listen(
async fn connect_and_login(
id: &str,
+ password: &str,
ui_receiver: &mut mpsc::UnboundedReceiver,
interface: impl Interface,
forward: &mut Framed,
@@ -120,21 +123,21 @@ async fn connect_and_login(
Ok(Some(Ok(bytes))) => {
let msg_in = Message::parse_from_bytes(&bytes)?;
match msg_in.union {
- Some(message::Union::hash(hash)) => {
- interface.handle_hash(hash, &mut stream).await;
+ Some(message::Union::Hash(hash)) => {
+ interface.handle_hash(password, hash, &mut stream).await;
}
- Some(message::Union::login_response(lr)) => match lr.union {
- Some(login_response::Union::error(err)) => {
+ Some(message::Union::LoginResponse(lr)) => match lr.union {
+ Some(login_response::Union::Error(err)) => {
interface.handle_login_error(&err);
return Ok(None);
}
- Some(login_response::Union::peer_info(pi)) => {
+ Some(login_response::Union::PeerInfo(pi)) => {
interface.handle_peer_info(pi);
break;
}
_ => {}
}
- Some(message::Union::test_delay(t)) => {
+ Some(message::Union::TestDelay(t)) => {
interface.handle_test_delay(t, &mut stream).await;
}
_ => {}
@@ -183,7 +186,7 @@ async fn run_forward(forward: Framed, stream: Stream) ->
},
res = stream.next() => {
if let Some(Ok(bytes)) = res {
- allow_err!(forward.send(bytes.into()).await);
+ allow_err!(forward.send(bytes).await);
} else {
break;
}
diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs
index 09500804b..6e38bff21 100644
--- a/src/rendezvous_mediator.rs
+++ b/src/rendezvous_mediator.rs
@@ -1,21 +1,4 @@
-use crate::server::{check_zombie, new as new_server, ServerPtr};
-use hbb_common::{
- allow_err,
- anyhow::bail,
- config::{self, Config, REG_INTERVAL, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT},
- futures::future::join_all,
- log,
- protobuf::Message as _,
- rendezvous_proto::*,
- sleep, socket_client,
- tcp::FramedStream,
- tokio::{
- self, select,
- time::{interval, Duration},
- },
- udp::FramedSocket,
- AddrMangle, IntoTargetAddr, ResultType, TargetAddr,
-};
+use std::collections::HashMap;
use std::{
net::SocketAddr,
sync::{
@@ -24,8 +7,31 @@ use std::{
},
time::Instant,
};
+
use uuid::Uuid;
+use hbb_common::config::DiscoveryPeer;
+use hbb_common::tcp::FramedStream;
+use hbb_common::{
+ allow_err,
+ anyhow::bail,
+ config,
+ config::{Config, REG_INTERVAL, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT},
+ futures::future::join_all,
+ log,
+ protobuf::Message as _,
+ rendezvous_proto::*,
+ sleep, socket_client,
+ tokio::{
+ self, select,
+ time::{interval, Duration},
+ },
+ udp::FramedSocket,
+ AddrMangle, IntoTargetAddr, ResultType, TargetAddr,
+};
+
+use crate::server::{check_zombie, new as new_server, ServerPtr};
+
type Message = RendezvousMessage;
lazy_static::lazy_static! {
@@ -52,9 +58,12 @@ impl RendezvousMediator {
check_zombie();
let server = new_server();
if Config::get_nat_type() == NatType::UNKNOWN_NAT as i32 {
- crate::common::test_nat_type();
+ crate::test_nat_type();
nat_tested = true;
}
+ if !Config::get_option("stop-service").is_empty() {
+ crate::test_rendezvous_server();
+ }
let server_cloned = server.clone();
tokio::spawn(async move {
direct_server(server_cloned).await;
@@ -62,14 +71,14 @@ impl RendezvousMediator {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if crate::platform::is_installed() {
std::thread::spawn(move || {
- allow_err!(lan_discovery());
+ allow_err!(super::lan::start_listening());
});
}
loop {
Config::reset_online();
if Config::get_option("stop-service").is_empty() {
if !nat_tested {
- crate::common::test_nat_type();
+ crate::test_nat_type();
nat_tested = true;
}
let mut futs = Vec::new();
@@ -158,7 +167,7 @@ impl RendezvousMediator {
Some(Ok((bytes, _))) => {
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
match msg_in.union {
- Some(rendezvous_message::Union::register_peer_response(rpr)) => {
+ Some(rendezvous_message::Union::RegisterPeerResponse(rpr)) => {
update_latency();
if rpr.request_pk {
log::info!("request_pk received from {}", host);
@@ -166,7 +175,7 @@ impl RendezvousMediator {
continue;
}
}
- Some(rendezvous_message::Union::register_pk_response(rpr)) => {
+ Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => {
update_latency();
match rpr.result.enum_value_or_default() {
register_pk_response::Result::OK => {
@@ -180,28 +189,28 @@ impl RendezvousMediator {
_ => {}
}
}
- Some(rendezvous_message::Union::punch_hole(ph)) => {
+ Some(rendezvous_message::Union::PunchHole(ph)) => {
let rz = rz.clone();
let server = server.clone();
tokio::spawn(async move {
allow_err!(rz.handle_punch_hole(ph, server).await);
});
}
- Some(rendezvous_message::Union::request_relay(rr)) => {
+ Some(rendezvous_message::Union::RequestRelay(rr)) => {
let rz = rz.clone();
let server = server.clone();
tokio::spawn(async move {
allow_err!(rz.handle_request_relay(rr, server).await);
});
}
- Some(rendezvous_message::Union::fetch_local_addr(fla)) => {
+ Some(rendezvous_message::Union::FetchLocalAddr(fla)) => {
let rz = rz.clone();
let server = server.clone();
tokio::spawn(async move {
allow_err!(rz.handle_intranet(fla, server).await);
});
}
- Some(rendezvous_message::Union::configure_update(cu)) => {
+ Some(rendezvous_message::Union::ConfigureUpdate(cu)) => {
let v0 = Config::get_rendezvous_servers();
Config::set_option("rendezvous-servers".to_owned(), cu.rendezvous_servers.join(","));
Config::set_serial(cu.serial);
@@ -264,7 +273,7 @@ impl RendezvousMediator {
async fn handle_request_relay(&self, rr: RequestRelay, server: ServerPtr) -> ResultType<()> {
self.create_relay(
- rr.socket_addr,
+ rr.socket_addr.into(),
rr.relay_server,
rr.uuid,
server,
@@ -301,7 +310,7 @@ impl RendezvousMediator {
let mut msg_out = Message::new();
let mut rr = RelayResponse {
- socket_addr,
+ socket_addr: socket_addr.into(),
version: crate::VERSION.to_owned(),
..Default::default()
};
@@ -332,8 +341,8 @@ impl RendezvousMediator {
let relay_server = self.get_relay_server(fla.relay_server);
msg_out.set_local_addr(LocalAddr {
id: Config::get_id(),
- socket_addr: AddrMangle::encode(peer_addr),
- local_addr: AddrMangle::encode(local_addr),
+ socket_addr: AddrMangle::encode(peer_addr).into(),
+ local_addr: AddrMangle::encode(local_addr).into(),
relay_server,
version: crate::VERSION.to_owned(),
..Default::default()
@@ -351,7 +360,14 @@ impl RendezvousMediator {
{
let uuid = Uuid::new_v4().to_string();
return self
- .create_relay(ph.socket_addr, relay_server, uuid, server, true, true)
+ .create_relay(
+ ph.socket_addr.into(),
+ relay_server,
+ uuid,
+ server,
+ true,
+ true,
+ )
.await;
}
let peer_addr = AddrMangle::decode(&ph.socket_addr);
@@ -368,7 +384,7 @@ impl RendezvousMediator {
socket
};
let mut msg_out = Message::new();
- use hbb_common::protobuf::ProtobufEnum;
+ use hbb_common::protobuf::Enum;
let nat_type = NatType::from_i32(Config::get_nat_type()).unwrap_or(NatType::UNKNOWN_NAT);
msg_out.set_punch_hole_sent(PunchHoleSent {
socket_addr: ph.socket_addr,
@@ -387,13 +403,13 @@ impl RendezvousMediator {
async fn register_pk(&mut self, socket: &mut FramedSocket) -> ResultType<()> {
let mut msg_out = Message::new();
let pk = Config::get_key_pair().1;
- let uuid = crate::get_uuid();
+ let uuid = hbb_common::get_uuid();
let id = Config::get_id();
self.last_id_pk_registry = id.clone();
msg_out.set_register_pk(RegisterPk {
id,
- uuid,
- pk,
+ uuid: uuid.into(),
+ pk: pk.into(),
..Default::default()
});
socket.send(&msg_out, self.addr.to_owned()).await?;
@@ -565,7 +581,7 @@ fn lan_discovery() -> ResultType<()> {
if let Ok((len, addr)) = socket.recv_from(&mut buf) {
if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) {
match msg_in.union {
- Some(rendezvous_message::Union::peer_discovery(p)) => {
+ Some(rendezvous_message::Union::PeerDiscovery(p)) => {
if p.cmd == "ping" {
let mut msg_out = Message::new();
let peer = PeerDiscovery {
@@ -613,11 +629,22 @@ pub fn discover() -> ResultType<()> {
if let Ok((len, _)) = socket.recv_from(&mut buf) {
if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) {
match msg_in.union {
- Some(rendezvous_message::Union::peer_discovery(p)) => {
+ Some(rendezvous_message::Union::PeerDiscovery(p)) => {
last_recv_time = Instant::now();
if p.cmd == "pong" {
if p.mac != mac {
- peers.push((p.id, p.username, p.hostname, p.platform));
+ let dp = DiscoveryPeer {
+ id: "".to_string(),
+ ip_mac: HashMap::from([
+ // TODO: addr ip
+ (addr.ip().to_string(), p.mac.clone()),
+ ]),
+ username: p.username,
+ hostname: p.hostname,
+ platform: p.platform,
+ online: true,
+ };
+ peers.push(dp);
}
}
}
@@ -626,7 +653,7 @@ pub fn discover() -> ResultType<()> {
}
}
if last_write_time.elapsed().as_millis() > 300 && last_write_n != peers.len() {
- config::LanPeers::store(serde_json::to_string(&peers)?);
+ config::LanPeers::store(&peers);
last_write_time = Instant::now();
last_write_n = peers.len();
}
@@ -635,7 +662,7 @@ pub fn discover() -> ResultType<()> {
}
}
log::info!("discover ping done");
- config::LanPeers::store(serde_json::to_string(&peers)?);
+ config::LanPeers::store(&peers);
Ok(())
}
@@ -675,7 +702,7 @@ pub async fn query_online_states, Vec)>(ids: Vec ResultType {
- let rendezvous_server = crate::get_rendezvous_server(1_000).await;
+ let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await;
let tmp: Vec<&str> = rendezvous_server.split(":").collect();
if tmp.len() != 2 {
bail!("Invalid server address: {}", rendezvous_server);
@@ -719,7 +746,7 @@ async fn query_online_states_(
Some(Ok(bytes)) => {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
match msg_in.union {
- Some(rendezvous_message::Union::online_response(online_response)) => {
+ Some(rendezvous_message::Union::OnlineResponse(online_response)) => {
let states = online_response.states;
let mut onlines = Vec::new();
let mut offlines = Vec::new();
diff --git a/src/server.rs b/src/server.rs
index 2b4be9f14..12b5afe56 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -7,7 +7,7 @@ use hbb_common::{
config::{Config, Config2, CONNECT_TIMEOUT, RELAY_PORT},
log,
message_proto::*,
- protobuf::{Message as _, ProtobufEnum},
+ protobuf::{Enum, Message as _},
rendezvous_proto::*,
socket_client,
sodiumoxide::crypto::{box_, secretbox, sign},
@@ -20,10 +20,16 @@ use std::{
sync::{Arc, Mutex, RwLock, Weak},
time::Duration,
};
+use bytes::Bytes;
+
pub mod audio_service;
cfg_if::cfg_if! {
if #[cfg(not(any(target_os = "android", target_os = "ios")))] {
mod clipboard_service;
+#[cfg(target_os = "linux")]
+mod wayland;
+#[cfg(target_os = "linux")]
+pub mod uinput;
pub mod input_service;
} else {
mod clipboard_service {
@@ -38,6 +44,7 @@ pub const NAME_POS: &'static str = "";
mod connection;
mod service;
+mod video_qos;
pub mod video_service;
use hbb_common::tcp::new_listener;
@@ -94,7 +101,7 @@ async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) ->
async fn check_privacy_mode_on(stream: &mut Stream) -> ResultType<()> {
if video_service::get_privacy_mode_conn_id() > 0 {
let msg_out =
- crate::common::make_privacy_mode_msg(back_notification::PrivacyModeState::OnByOther);
+ crate::common::make_privacy_mode_msg(back_notification::PrivacyModeState::PrvOnByOther);
timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??;
}
Ok(())
@@ -125,13 +132,13 @@ pub async fn create_tcp_connection(
id: sign::sign(
&IdPk {
id: Config::get_id(),
- pk: our_pk_b.0.to_vec(),
+ pk: Bytes::from(our_pk_b.0.to_vec()),
..Default::default()
}
.write_to_bytes()
.unwrap_or_default(),
&sk,
- ),
+ ).into(),
..Default::default()
});
timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??;
@@ -139,7 +146,7 @@ pub async fn create_tcp_connection(
Some(res) => {
let bytes = res?;
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
- if let Some(message::Union::public_key(pk)) = msg_in.union {
+ if let Some(message::Union::PublicKey(pk)) = msg_in.union {
if pk.asymmetric_value.len() == box_::PUBLICKEYBYTES {
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
let mut pk_ = [0u8; box_::PUBLICKEYBYTES];
@@ -278,6 +285,8 @@ impl Drop for Server {
for s in self.services.values() {
s.join();
}
+ #[cfg(target_os = "linux")]
+ wayland::clear();
}
}
@@ -326,6 +335,14 @@ pub async fn start_server(is_server: bool) {
log::info!("DISPLAY={:?}", std::env::var("DISPLAY"));
log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY"));
}
+ #[cfg(feature = "hwcodec")]
+ {
+ use std::sync::Once;
+ static ONCE: Once = Once::new();
+ ONCE.call_once(|| {
+ scrap::hwcodec::check_config_process(false);
+ })
+ }
if is_server {
std::thread::spawn(move || {
diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs
index e0974a228..addc06644 100644
--- a/src/server/audio_service.rs
+++ b/src/server/audio_service.rs
@@ -347,7 +347,7 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) {
Ok(data) => {
let mut msg_out = Message::new();
msg_out.set_audio_frame(AudioFrame {
- data,
+ data: data.into(),
timestamp: crate::common::get_time(),
..Default::default()
});
@@ -367,7 +367,7 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) {
Ok(data) => {
let mut msg_out = Message::new();
msg_out.set_audio_frame(AudioFrame {
- data,
+ data: data.into(),
timestamp: crate::common::get_time(),
..Default::default()
});
diff --git a/src/server/connection.rs b/src/server/connection.rs
index 967140a1d..fc38ec77f 100644
--- a/src/server/connection.rs
+++ b/src/server/connection.rs
@@ -3,17 +3,18 @@ use super::{input_service::*, *};
use crate::clipboard_file::*;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::common::update_clipboard;
+use crate::video_service;
#[cfg(any(target_os = "android", target_os = "ios"))]
use crate::{common::MOBILE_INFO2, flutter::connection_manager::start_channel};
use crate::{ipc, VERSION};
-use hbb_common::fs::can_enable_overwrite_detection;
use hbb_common::{
config::Config,
fs,
+ fs::can_enable_overwrite_detection,
futures::{SinkExt, StreamExt},
get_version_number,
message_proto::{option_message::BoolOption, permission_info::Permission},
- sleep, timeout,
+ password_security as password, sleep, timeout,
tokio::{
net::TcpStream,
sync::mpsc,
@@ -29,11 +30,14 @@ use std::sync::{
atomic::{AtomicI64, Ordering},
mpsc as std_mpsc,
};
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+use system_shutdown;
pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>;
lazy_static::lazy_static! {
static ref LOGIN_FAILURES: Arc::>> = Default::default();
+ static ref SESSIONS: Arc::>> = Default::default();
}
pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0);
pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0);
@@ -52,6 +56,14 @@ enum MessageInput {
BlockOff,
}
+#[derive(Clone, Debug)]
+struct Session {
+ name: String,
+ session_id: u64,
+ last_recv_time: Arc>,
+ random_password: String,
+}
+
pub struct Connection {
inner: ConnInner,
stream: super::Stream,
@@ -68,8 +80,8 @@ pub struct Connection {
clipboard: bool,
audio: bool,
file: bool,
+ restart: bool,
last_test_delay: i64,
- image_quality: i32,
lock_after_session_end: bool,
show_remote_cursor: bool, // by peer
ip: String,
@@ -80,6 +92,8 @@ pub struct Connection {
video_ack_required: bool,
peer_info: (String, String),
api_server: String,
+ lr: LoginRequest,
+ last_recv_time: Arc>,
}
impl Subscriber for ConnInner {
@@ -91,7 +105,7 @@ impl Subscriber for ConnInner {
#[inline]
fn send(&mut self, msg: Arc) {
match &msg.union {
- Some(message::Union::video_frame(_)) => {
+ Some(message::Union::VideoFrame(_)) => {
self.tx_video.as_mut().map(|tx| {
allow_err!(tx.send((Instant::now(), msg)));
});
@@ -105,12 +119,13 @@ impl Subscriber for ConnInner {
}
}
-const TEST_DELAY_TIMEOUT: Duration = Duration::from_secs(3);
+const TEST_DELAY_TIMEOUT: Duration = Duration::from_secs(1);
const SEC30: Duration = Duration::from_secs(30);
const H1: Duration = Duration::from_secs(3600);
const MILLI1: Duration = Duration::from_millis(1);
const SEND_TIMEOUT_VIDEO: u64 = 12_000;
const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10;
+const SESSION_TIMEOUT: Duration = Duration::from_secs(30);
impl Connection {
pub async fn start(
@@ -121,7 +136,7 @@ impl Connection {
) {
let hash = Hash {
salt: Config::get_salt(),
- challenge: Config::get_auto_password(),
+ challenge: Config::get_auto_password(6),
..Default::default()
};
let (tx_from_cm_holder, mut rx_from_cm) = mpsc::unbounded_channel::();
@@ -153,8 +168,8 @@ impl Connection {
clipboard: Config::get_option("enable-clipboard").is_empty(),
audio: Config::get_option("enable-audio").is_empty(),
file: Config::get_option("enable-file-transfer").is_empty(),
+ restart: Config::get_option("enable-remote-restart").is_empty(),
last_test_delay: 0,
- image_quality: ImageQuality::Balanced.value(),
lock_after_session_end: false,
show_remote_cursor: false,
ip: "".to_owned(),
@@ -165,6 +180,8 @@ impl Connection {
video_ack_required: false,
peer_info: Default::default(),
api_server: "".to_owned(),
+ lr: Default::default(),
+ last_recv_time: Arc::new(Mutex::new(Instant::now())),
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
tokio::spawn(async move {
@@ -190,6 +207,9 @@ impl Connection {
if !conn.file {
conn.send_permission(Permission::File, false).await;
}
+ if !conn.restart {
+ conn.send_permission(Permission::Restart, false).await;
+ }
let mut test_delay_timer =
time::interval_at(Instant::now() + TEST_DELAY_TIMEOUT, TEST_DELAY_TIMEOUT);
let mut last_recv_time = Instant::now();
@@ -223,7 +243,8 @@ impl Connection {
let mut msg_out = Message::new();
msg_out.set_misc(misc);
conn.send(msg_out).await;
- conn.on_close("Close requested from connection manager", false);
+ conn.on_close("Close requested from connection manager", false).await;
+ SESSIONS.lock().unwrap().remove(&conn.lr.my_id);
break;
}
ipc::Data::ChatMessage{text} => {
@@ -266,6 +287,9 @@ impl Connection {
conn.file = enabled;
conn.send_permission(Permission::File, enabled).await;
conn.send_to_cm(ipc::Data::ClipboardFileEnabled(conn.file_transfer_enabled()));
+ } else if &name == "restart" {
+ conn.restart = enabled;
+ conn.send_permission(Permission::Restart, enabled).await;
}
}
ipc::Data::RawMessage(bytes) => {
@@ -282,24 +306,24 @@ impl Connection {
ipc::PrivacyModeState::OffSucceeded => {
video_service::set_privacy_mode_conn_id(0);
crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::OffSucceeded,
+ back_notification::PrivacyModeState::PrvOffSucceeded,
)
}
ipc::PrivacyModeState::OffFailed => {
crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::OffFailed,
+ back_notification::PrivacyModeState::PrvOffFailed,
)
}
ipc::PrivacyModeState::OffByPeer => {
video_service::set_privacy_mode_conn_id(0);
crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::OffByPeer,
+ back_notification::PrivacyModeState::PrvOffByPeer,
)
}
ipc::PrivacyModeState::OffUnknown => {
video_service::set_privacy_mode_conn_id(0);
crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::OffUnknown,
+ back_notification::PrivacyModeState::PrvOffUnknown,
)
}
};
@@ -312,11 +336,12 @@ impl Connection {
if let Some(res) = res {
match res {
Err(err) => {
- conn.on_close(&err.to_string(), true);
+ conn.on_close(&err.to_string(), true).await;
break;
},
Ok(bytes) => {
last_recv_time = Instant::now();
+ *conn.last_recv_time.lock().unwrap() = Instant::now();
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
if !conn.on_message(msg_in).await {
break;
@@ -325,14 +350,14 @@ impl Connection {
}
}
} else {
- conn.on_close("Reset by the peer", true);
+ conn.on_close("Reset by the peer", true).await;
break;
}
},
_ = conn.timer.tick() => {
if !conn.read_jobs.is_empty() {
if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await {
- conn.on_close(&err.to_string(), false);
+ conn.on_close(&err.to_string(), false).await;
break;
}
} else {
@@ -345,7 +370,7 @@ impl Connection {
video_service::notify_video_frame_feched(id, Some(instant.into()));
}
if let Err(err) = conn.stream.send(&value as &Message).await {
- conn.on_close(&err.to_string(), false);
+ conn.on_close(&err.to_string(), false).await;
break;
}
},
@@ -355,7 +380,7 @@ impl Connection {
if latency > 1000 {
match &msg.union {
- Some(message::Union::audio_frame(_)) => {
+ Some(message::Union::AudioFrame(_)) => {
// log::info!("audio frame latency {}", instant.elapsed().as_secs_f32());
continue;
}
@@ -363,21 +388,24 @@ impl Connection {
}
}
if let Err(err) = conn.stream.send(msg).await {
- conn.on_close(&err.to_string(), false);
+ conn.on_close(&err.to_string(), false).await;
break;
}
},
_ = test_delay_timer.tick() => {
if last_recv_time.elapsed() >= SEC30 {
- conn.on_close("Timeout", true);
+ conn.on_close("Timeout", true).await;
break;
}
let time = crate::get_time();
if time > 0 && conn.last_test_delay == 0 {
conn.last_test_delay = time;
let mut msg_out = Message::new();
+ let qos = video_service::VIDEO_QOS.lock().unwrap();
msg_out.set_test_delay(TestDelay{
time,
+ last_delay:qos.current_delay,
+ target_bitrate:qos.target_bitrate,
..Default::default()
});
conn.inner.send(msg_out.into());
@@ -394,10 +422,13 @@ impl Connection {
let _ = privacy_mode::turn_off_privacy(0);
}
video_service::notify_video_frame_feched(id, None);
- video_service::update_test_latency(id, 0);
- video_service::update_image_quality(id, None);
+ scrap::codec::Encoder::update_video_encoder(id, scrap::codec::EncoderUpdate::Remove);
+ video_service::VIDEO_QOS.lock().unwrap().reset();
+ if conn.authorized {
+ password::update_temporary_password();
+ }
if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await {
- conn.on_close(&err.to_string(), false);
+ conn.on_close(&err.to_string(), false).await;
}
conn.post_audit(json!({
@@ -432,7 +463,7 @@ impl Connection {
} else {
Self::send_block_input_error(
&tx,
- back_notification::BlockInputState::OnFailed,
+ back_notification::BlockInputState::BlkOnFailed,
);
}
}
@@ -442,7 +473,7 @@ impl Connection {
} else {
Self::send_block_input_error(
&tx,
- back_notification::BlockInputState::OffFailed,
+ back_notification::BlockInputState::BlkOffFailed,
);
}
}
@@ -490,7 +521,7 @@ impl Connection {
res = self.stream.next() => {
if let Some(res) = res {
last_recv_time = Instant::now();
- timeout(SEND_TIMEOUT_OTHER, forward.send(res?.into())).await??;
+ timeout(SEND_TIMEOUT_OTHER, forward.send(res?)).await??;
} else {
bail!("Stream reset by the peer");
}
@@ -569,7 +600,7 @@ impl Connection {
let url = self.api_server.clone();
let mut v = v;
v["id"] = json!(Config::get_id());
- v["uuid"] = json!(base64::encode(crate::get_uuid()));
+ v["uuid"] = json!(base64::encode(hbb_common::get_uuid()));
v["Id"] = json!(self.inner.id);
tokio::spawn(async move {
allow_err!(Self::post_audit_async(url, v).await);
@@ -615,6 +646,16 @@ impl Connection {
pi.hostname = MOBILE_INFO2.lock().unwrap().clone();
pi.platform = "Android".into();
}
+ #[cfg(feature = "hwcodec")]
+ {
+ let (h264, h265) = scrap::codec::Encoder::supported_encoding();
+ pi.encoding = Some(SupportedEncoding {
+ h264,
+ h265,
+ ..Default::default()
+ })
+ .into();
+ }
if self.port_forward_socket.is_some() {
let mut msg_out = Message::new();
@@ -626,9 +667,9 @@ impl Connection {
#[cfg(target_os = "linux")]
if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() {
let dtype = crate::platform::linux::get_display_server();
- if dtype != "x11" {
+ if dtype != "x11" && dtype != "wayland" {
res.set_error(format!(
- "Unsupported display server type {}, x11 expected",
+ "Unsupported display server type {}, x11 or wayland expected",
dtype
));
let mut msg_out = Message::new();
@@ -664,7 +705,7 @@ impl Connection {
res.set_peer_info(pi);
} else {
try_activate_screen();
- match super::video_service::get_displays() {
+ match super::video_service::get_displays().await {
Err(err) => {
res.set_error(format!("X11 error: {}", err));
}
@@ -734,6 +775,7 @@ impl Connection {
audio: self.audio,
file: self.file,
file_transfer_enabled: self.file_transfer_enabled(),
+ restart: self.restart,
});
}
@@ -776,17 +818,99 @@ impl Connection {
self.tx_input.send(MessageInput::Key((msg, press))).ok();
}
+ fn validate_one_password(&self, password: String) -> bool {
+ if password.len() == 0 {
+ return false;
+ }
+ let mut hasher = Sha256::new();
+ hasher.update(password);
+ hasher.update(&self.hash.salt);
+ let mut hasher2 = Sha256::new();
+ hasher2.update(&hasher.finalize()[..]);
+ hasher2.update(&self.hash.challenge);
+ hasher2.finalize()[..] == self.lr.password[..]
+ }
+
+ fn validate_password(&mut self) -> bool {
+ if password::temporary_enabled() {
+ let password = password::temporary_password();
+ if self.validate_one_password(password.clone()) {
+ SESSIONS.lock().unwrap().insert(
+ self.lr.my_id.clone(),
+ Session {
+ name: self.lr.my_name.clone(),
+ session_id: self.lr.session_id,
+ last_recv_time: self.last_recv_time.clone(),
+ random_password: password,
+ },
+ );
+ return true;
+ }
+ }
+ if password::permanent_enabled() {
+ if self.validate_one_password(Config::get_permanent_password()) {
+ return true;
+ }
+ }
+ false
+ }
+
+ fn is_of_recent_session(&mut self) -> bool {
+ let session = SESSIONS
+ .lock()
+ .unwrap()
+ .get(&self.lr.my_id)
+ .map(|s| s.to_owned());
+ if let Some(session) = session {
+ if session.name == self.lr.my_name
+ && session.session_id == self.lr.session_id
+ && !self.lr.password.is_empty()
+ && self.validate_one_password(session.random_password.clone())
+ && session.last_recv_time.lock().unwrap().elapsed() < SESSION_TIMEOUT
+ {
+ SESSIONS.lock().unwrap().insert(
+ self.lr.my_id.clone(),
+ Session {
+ name: self.lr.my_name.clone(),
+ session_id: self.lr.session_id,
+ last_recv_time: self.last_recv_time.clone(),
+ random_password: session.random_password,
+ },
+ );
+ return true;
+ }
+ }
+ false
+ }
+
async fn on_message(&mut self, msg: Message) -> bool {
- if let Some(message::Union::login_request(lr)) = msg.union {
+ if let Some(message::Union::LoginRequest(lr)) = msg.union {
+ self.lr = lr.clone();
if let Some(o) = lr.option.as_ref() {
self.update_option(o).await;
+ if let Some(q) = o.video_codec_state.clone().take() {
+ scrap::codec::Encoder::update_video_encoder(
+ self.inner.id(),
+ scrap::codec::EncoderUpdate::State(q),
+ );
+ } else {
+ scrap::codec::Encoder::update_video_encoder(
+ self.inner.id(),
+ scrap::codec::EncoderUpdate::DisableHwIfNotExist,
+ );
+ }
+ } else {
+ scrap::codec::Encoder::update_video_encoder(
+ self.inner.id(),
+ scrap::codec::EncoderUpdate::DisableHwIfNotExist,
+ );
}
self.video_ack_required = lr.video_ack_required;
if self.authorized {
return true;
}
match lr.union {
- Some(login_request::Union::file_transfer(ft)) => {
+ Some(login_request::Union::FileTransfer(ft)) => {
if !Config::get_option("enable-file-transfer").is_empty() {
self.send_login_error("No permission of file transfer")
.await;
@@ -795,7 +919,7 @@ impl Connection {
}
self.file_transfer = Some((ft.dir, ft.show_hidden));
}
- Some(login_request::Union::port_forward(mut pf)) => {
+ Some(login_request::Union::PortForward(mut pf)) => {
if !Config::get_option("enable-tunnel").is_empty() {
self.send_login_error("No permission of IP tunneling").await;
sleep(1.).await;
@@ -832,15 +956,19 @@ impl Connection {
}
if !crate::is_ip(&lr.username) && lr.username != Config::get_id() {
self.send_login_error("Offline").await;
+ } else if self.is_of_recent_session() {
+ self.try_start_cm(lr.my_id, lr.my_name, true);
+ self.send_logon_response().await;
+ if self.port_forward_socket.is_some() {
+ return false;
+ }
} else if lr.password.is_empty() {
self.try_start_cm(lr.my_id, lr.my_name, false);
} else {
- let mut hasher = Sha256::new();
- hasher.update(&Config::get_password());
- hasher.update(&self.hash.salt);
- let mut hasher2 = Sha256::new();
- hasher2.update(&hasher.finalize()[..]);
- hasher2.update(&self.hash.challenge);
+ if !password::has_valid_password() {
+ self.send_login_error("Connection not allowed").await;
+ return false;
+ }
let mut failure = LOGIN_FAILURES
.lock()
.unwrap()
@@ -853,7 +981,7 @@ impl Connection {
.await;
} else if time == failure.0 && failure.1 > 6 {
self.send_login_error("Please try 1 minute later").await;
- } else if hasher2.finalize()[..] != lr.password[..] {
+ } else if !self.validate_password() {
if failure.0 == time {
failure.1 += 1;
failure.2 += 1;
@@ -879,21 +1007,22 @@ impl Connection {
}
}
}
- } else if let Some(message::Union::test_delay(t)) = msg.union {
+ } else if let Some(message::Union::TestDelay(t)) = msg.union {
if t.from_client {
let mut msg_out = Message::new();
msg_out.set_test_delay(t);
self.inner.send(msg_out.into());
} else {
self.last_test_delay = 0;
- let latency = crate::get_time() - t.time;
- if latency > 0 {
- super::video_service::update_test_latency(self.inner.id(), latency);
- }
+ let new_delay = (crate::get_time() - t.time) as u32;
+ video_service::VIDEO_QOS
+ .lock()
+ .unwrap()
+ .update_network_delay(new_delay);
}
} else if self.authorized {
match msg.union {
- Some(message::Union::mouse_event(me)) => {
+ Some(message::Union::MouseEvent(me)) => {
#[cfg(any(target_os = "android", target_os = "ios"))]
if let Err(e) = call_main_service_mouse_input(me.mask, me.x, me.y) {
log::debug!("call_main_service_mouse_input fail:{}", e);
@@ -908,7 +1037,7 @@ impl Connection {
self.input_mouse(me, self.inner.id());
}
}
- Some(message::Union::key_event(me)) => {
+ Some(message::Union::KeyEvent(me)) => {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if self.keyboard {
if is_enter(&me) {
@@ -924,8 +1053,8 @@ impl Connection {
};
if is_press {
match me.union {
- Some(key_event::Union::unicode(_))
- | Some(key_event::Union::seq(_)) => {
+ Some(key_event::Union::Unicode(_))
+ | Some(key_event::Union::Seq(_)) => {
self.input_key(me, false);
}
_ => {
@@ -937,14 +1066,14 @@ impl Connection {
}
}
}
- Some(message::Union::clipboard(cb)) =>
+ Some(message::Union::Clipboard(cb)) =>
{
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if self.clipboard {
update_clipboard(cb, None);
}
}
- Some(message::Union::cliprdr(_clip)) => {
+ Some(message::Union::Cliprdr(_clip)) => {
if self.file_transfer_enabled() {
#[cfg(windows)]
if let Some(clip) = msg_2_clip(_clip) {
@@ -952,13 +1081,13 @@ impl Connection {
}
}
}
- Some(message::Union::file_action(fa)) => {
+ Some(message::Union::FileAction(fa)) => {
if self.file_transfer.is_some() {
match fa.union {
- Some(file_action::Union::read_dir(rd)) => {
+ Some(file_action::Union::ReadDir(rd)) => {
self.read_dir(&rd.path, rd.include_hidden);
}
- Some(file_action::Union::all_files(f)) => {
+ Some(file_action::Union::AllFiles(f)) => {
match fs::get_recursive_files(&f.path, f.include_hidden) {
Err(err) => {
self.send(fs::new_error(f.id, err, -1)).await;
@@ -968,7 +1097,7 @@ impl Connection {
}
}
}
- Some(file_action::Union::send(s)) => {
+ Some(file_action::Union::Send(s)) => {
let id = s.id;
let od =
can_enable_overwrite_detection(get_version_number(VERSION));
@@ -993,7 +1122,7 @@ impl Connection {
}
}
}
- Some(file_action::Union::receive(r)) => {
+ Some(file_action::Union::Receive(r)) => {
self.send_fs(ipc::FS::NewWrite {
path: r.path,
id: r.id,
@@ -1006,31 +1135,31 @@ impl Connection {
.collect(),
});
}
- Some(file_action::Union::remove_dir(d)) => {
+ Some(file_action::Union::RemoveDir(d)) => {
self.send_fs(ipc::FS::RemoveDir {
path: d.path,
id: d.id,
recursive: d.recursive,
});
}
- Some(file_action::Union::remove_file(f)) => {
+ Some(file_action::Union::RemoveFile(f)) => {
self.send_fs(ipc::FS::RemoveFile {
path: f.path,
id: f.id,
file_num: f.file_num,
});
}
- Some(file_action::Union::create(c)) => {
+ Some(file_action::Union::Create(c)) => {
self.send_fs(ipc::FS::CreateDir {
path: c.path,
id: c.id,
});
}
- Some(file_action::Union::cancel(c)) => {
+ Some(file_action::Union::Cancel(c)) => {
self.send_fs(ipc::FS::CancelWrite { id: c.id });
fs::remove_job(c.id, &mut self.read_jobs);
}
- Some(file_action::Union::send_confirm(r)) => {
+ Some(file_action::Union::SendConfirm(r)) => {
if let Some(job) = fs::get_job(r.id, &mut self.read_jobs) {
job.confirm(&r);
}
@@ -1039,8 +1168,8 @@ impl Connection {
}
}
}
- Some(message::Union::file_response(fr)) => match fr.union {
- Some(file_response::Union::block(block)) => {
+ Some(message::Union::FileResponse(fr)) => match fr.union {
+ Some(file_response::Union::Block(block)) => {
self.send_fs(ipc::FS::WriteBlock {
id: block.id,
file_num: block.file_num,
@@ -1048,13 +1177,13 @@ impl Connection {
compressed: block.compressed,
});
}
- Some(file_response::Union::done(d)) => {
+ Some(file_response::Union::Done(d)) => {
self.send_fs(ipc::FS::WriteDone {
id: d.id,
file_num: d.file_num,
});
}
- Some(file_response::Union::digest(d)) => self.send_fs(ipc::FS::CheckDigest {
+ Some(file_response::Union::Digest(d)) => self.send_fs(ipc::FS::CheckDigest {
id: d.id,
file_num: d.file_num,
file_size: d.file_size,
@@ -1063,27 +1192,43 @@ impl Connection {
}),
_ => {}
},
- Some(message::Union::misc(misc)) => match misc.union {
- Some(misc::Union::switch_display(s)) => {
- super::video_service::switch_display(s.display);
+ Some(message::Union::Misc(misc)) => match misc.union {
+ Some(misc::Union::SwitchDisplay(s)) => {
+ video_service::switch_display(s.display).await;
}
- Some(misc::Union::chat_message(c)) => {
+ Some(misc::Union::ChatMessage(c)) => {
self.send_to_cm(ipc::Data::ChatMessage { text: c.text });
}
- Some(misc::Union::option(o)) => {
+ Some(misc::Union::Option(o)) => {
self.update_option(&o).await;
}
- Some(misc::Union::refresh_video(r)) => {
+ Some(misc::Union::RefreshVideo(r)) => {
if r {
super::video_service::refresh();
}
}
- Some(misc::Union::video_received(_)) => {
+ Some(misc::Union::VideoReceived(_)) => {
video_service::notify_video_frame_feched(
self.inner.id,
Some(Instant::now().into()),
);
}
+ Some(misc::Union::CloseReason(_)) => {
+ self.on_close("Peer close", true).await;
+ SESSIONS.lock().unwrap().remove(&self.lr.my_id);
+ return false;
+ }
+
+ Some(misc::Union::RestartRemoteDevice(_)) =>
+ {
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ if self.restart {
+ match system_shutdown::reboot() {
+ Ok(_) => log::info!("Restart by the peer"),
+ Err(e) => log::error!("Failed to restart:{}", e),
+ }
+ }
+ }
_ => {}
},
_ => {}
@@ -1095,13 +1240,20 @@ impl Connection {
async fn update_option(&mut self, o: &OptionMessage) {
log::info!("Option update: {:?}", o);
if let Ok(q) = o.image_quality.enum_value() {
- self.image_quality = q.value();
- super::video_service::update_image_quality(self.inner.id(), Some(q.value()));
- }
- let q = o.custom_image_quality;
- if q > 0 {
- self.image_quality = q;
- super::video_service::update_image_quality(self.inner.id(), Some(q));
+ let image_quality;
+ if let ImageQuality::NotSet = q {
+ if o.custom_image_quality > 0 {
+ image_quality = o.custom_image_quality;
+ } else {
+ image_quality = ImageQuality::Balanced.value();
+ }
+ } else {
+ image_quality = q.value();
+ }
+ video_service::VIDEO_QOS
+ .lock()
+ .unwrap()
+ .update_image_quality(image_quality);
}
if let Ok(q) = o.lock_after_session_end.enum_value() {
if q != BoolOption::NotSet {
@@ -1164,7 +1316,7 @@ impl Connection {
BoolOption::Yes => {
let msg_out = if !video_service::is_privacy_mode_supported() {
crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::NotSupported,
+ back_notification::PrivacyModeState::PrvNotSupported,
)
} else {
match privacy_mode::turn_on_privacy(self.inner.id) {
@@ -1172,7 +1324,7 @@ impl Connection {
if video_service::test_create_capturer(self.inner.id, 5_000) {
video_service::set_privacy_mode_conn_id(self.inner.id);
crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::OnSucceeded,
+ back_notification::PrivacyModeState::PrvOnSucceeded,
)
} else {
log::error!(
@@ -1181,12 +1333,12 @@ impl Connection {
video_service::set_privacy_mode_conn_id(0);
let _ = privacy_mode::turn_off_privacy(self.inner.id);
crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::OnFailed,
+ back_notification::PrivacyModeState::PrvOnFailed,
)
}
}
Ok(false) => crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::OnFailedPlugin,
+ back_notification::PrivacyModeState::PrvOnFailedPlugin,
),
Err(e) => {
log::error!("Failed to turn on privacy mode. {}", e);
@@ -1194,7 +1346,7 @@ impl Connection {
let _ = privacy_mode::turn_off_privacy(0);
}
crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::OnFailed,
+ back_notification::PrivacyModeState::PrvOnFailed,
)
}
}
@@ -1204,7 +1356,7 @@ impl Connection {
BoolOption::No => {
let msg_out = if !video_service::is_privacy_mode_supported() {
crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::NotSupported,
+ back_notification::PrivacyModeState::PrvNotSupported,
)
} else {
video_service::set_privacy_mode_conn_id(0);
@@ -1229,16 +1381,22 @@ impl Connection {
}
}
}
+ if let Some(q) = o.video_codec_state.clone().take() {
+ scrap::codec::Encoder::update_video_encoder(
+ self.inner.id(),
+ scrap::codec::EncoderUpdate::State(q),
+ );
+ }
}
- fn on_close(&mut self, reason: &str, lock: bool) {
+ async fn on_close(&mut self, reason: &str, lock: bool) {
if let Some(s) = self.server.upgrade() {
s.write().unwrap().remove_connection(&self.inner);
}
log::info!("#{} Connection closed: {}", self.inner.id(), reason);
if lock && self.lock_after_session_end && self.keyboard {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
- lock_screen();
+ lock_screen().await;
}
self.tx_to_cm.send(ipc::Data::Close).ok();
self.port_forward_socket.take();
@@ -1337,7 +1495,7 @@ async fn start_ipc(
file_num,
data,
compressed}) = data {
- stream.send(&Data::FS(ipc::FS::WriteBlock{id, file_num, data: Vec::new(), compressed})).await?;
+ stream.send(&Data::FS(ipc::FS::WriteBlock{id, file_num, data: Bytes::new(), compressed})).await?;
stream.send_raw(data).await?;
} else {
stream.send(&data).await?;
@@ -1373,19 +1531,19 @@ mod privacy_mode {
let res = turn_off_privacy(_conn_id, None);
match res {
Ok(_) => crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::OffSucceeded,
+ back_notification::PrivacyModeState::PrvOffSucceeded,
),
Err(e) => {
log::error!("Failed to turn off privacy mode {}", e);
crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::OffFailed,
+ back_notification::PrivacyModeState::PrvOffFailed,
)
}
}
}
#[cfg(not(windows))]
{
- crate::common::make_privacy_mode_msg(back_notification::PrivacyModeState::OffFailed)
+ crate::common::make_privacy_mode_msg(back_notification::PrivacyModeState::PrvOffFailed)
}
}
diff --git a/src/server/input_service.rs b/src/server/input_service.rs
index 8c5f3060b..e63b6290e 100644
--- a/src/server/input_service.rs
+++ b/src/server/input_service.rs
@@ -2,7 +2,7 @@ use super::*;
#[cfg(target_os = "macos")]
use dispatch::Queue;
use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable};
-use hbb_common::{config::COMPRESS_LEVEL, protobuf::ProtobufEnumOrUnknown};
+use hbb_common::{config::COMPRESS_LEVEL, protobuf::EnumOrUnknown};
use std::{
convert::TryFrom,
sync::atomic::{AtomicBool, Ordering},
@@ -68,7 +68,7 @@ impl Subscriber for MouseCursorSub {
#[inline]
fn send(&mut self, msg: Arc) {
- if let Some(message::Union::cursor_data(cd)) = &msg.union {
+ if let Some(message::Union::CursorData(cd)) = &msg.union {
if let Some(msg) = self.cached.get(&cd.id) {
self.inner.send(msg.clone());
} else {
@@ -145,7 +145,7 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()>
msg = cached.clone();
} else {
let mut data = crate::get_cursor_data(hcursor)?;
- data.colors = hbb_common::compress::compress(&data.colors[..], COMPRESS_LEVEL);
+ data.colors = hbb_common::compress::compress(&data.colors[..], COMPRESS_LEVEL).into();
let mut tmp = Message::new();
tmp.set_cursor_data(data);
msg = Arc::new(tmp);
@@ -187,6 +187,26 @@ lazy_static::lazy_static! {
static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned());
}
+#[cfg(target_os = "linux")]
+pub async fn set_uinput() -> ResultType<()> {
+ // Keyboard and mouse both open /dev/uinput
+ // TODO: Make sure there's no race
+ let keyboard = super::uinput::client::UInputKeyboard::new().await?;
+ log::info!("UInput keyboard created");
+ let mouse = super::uinput::client::UInputMouse::new().await?;
+ log::info!("UInput mouse created");
+
+ let mut en = ENIGO.lock().unwrap();
+ en.set_uinput_keyboard(Some(Box::new(keyboard)));
+ en.set_uinput_mouse(Some(Box::new(mouse)));
+ Ok(())
+}
+
+#[cfg(target_os = "linux")]
+pub async fn set_uinput_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> {
+ super::uinput::client::set_resolution(minx, maxx, miny, maxy).await
+}
+
pub fn is_left_up(evt: &MouseEvent) -> bool {
let buttons = evt.mask >> 3;
let evt_type = evt.mask & 0x7;
@@ -299,12 +319,12 @@ fn fix_key_down_timeout(force: bool) {
// e.g. current state of ctrl is down, but ctrl not in modifier, we should change ctrl to up, to make modifier state sync between remote and local
#[inline]
fn fix_modifier(
- modifiers: &[ProtobufEnumOrUnknown],
+ modifiers: &[EnumOrUnknown],
key0: ControlKey,
key1: Key,
en: &mut Enigo,
) {
- if get_modifier_state(key1, en) && !modifiers.contains(&ProtobufEnumOrUnknown::new(key0)) {
+ if get_modifier_state(key1, en) && !modifiers.contains(&EnumOrUnknown::new(key0)) {
#[cfg(windows)]
if key0 == ControlKey::Control && get_modifier_state(Key::Alt, en) {
// AltGr case
@@ -315,7 +335,7 @@ fn fix_modifier(
}
}
-fn fix_modifiers(modifiers: &[ProtobufEnumOrUnknown], en: &mut Enigo, ck: i32) {
+fn fix_modifiers(modifiers: &[EnumOrUnknown], en: &mut Enigo, ck: i32) {
if ck != ControlKey::Shift.value() {
fix_modifier(modifiers, ControlKey::Shift, Key::Shift, en);
}
@@ -430,7 +450,7 @@ fn handle_mouse_(evt: &MouseEvent, conn: i32) {
}
pub fn is_enter(evt: &KeyEvent) -> bool {
- if let Some(key_event::Union::control_key(ck)) = evt.union {
+ if let Some(key_event::Union::ControlKey(ck)) = evt.union {
if ck.value() == ControlKey::Return.value() || ck.value() == ControlKey::NumpadEnter.value()
{
return true;
@@ -439,7 +459,7 @@ pub fn is_enter(evt: &KeyEvent) -> bool {
return false;
}
-pub fn lock_screen() {
+pub async fn lock_screen() {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
// xdg_screensaver lock not work on Linux from our service somehow
@@ -469,7 +489,7 @@ pub fn lock_screen() {
crate::platform::lock_screen();
}
}
- super::video_service::switch_to_primary();
+ super::video_service::switch_to_primary().await;
}
lazy_static::lazy_static! {
@@ -548,7 +568,6 @@ lazy_static::lazy_static! {
(ControlKey::Equals, Key::Equals),
(ControlKey::NumpadEnter, Key::NumpadEnter),
(ControlKey::RAlt, Key::RightAlt),
- (ControlKey::RWin, Key::RWin),
(ControlKey::RControl, Key::RightControl),
(ControlKey::RShift, Key::RightShift),
].iter().map(|(a, b)| (a.value(), b.clone())).collect();
@@ -598,7 +617,7 @@ fn handle_key_(evt: &KeyEvent) {
#[cfg(windows)]
let mut has_numlock = false;
if evt.down {
- let ck = if let Some(key_event::Union::control_key(ck)) = evt.union {
+ let ck = if let Some(key_event::Union::ControlKey(ck)) = evt.union {
ck.value()
} else {
-1
@@ -653,7 +672,7 @@ fn handle_key_(evt: &KeyEvent) {
}
}
match evt.union {
- Some(key_event::Union::control_key(ck)) => {
+ Some(key_event::Union::ControlKey(ck)) => {
if let Some(key) = KEY_MAP.get(&ck.value()) {
#[cfg(windows)]
if let Some(_) = NUMPAD_KEY_MAP.get(&ck.value()) {
@@ -679,10 +698,10 @@ fn handle_key_(evt: &KeyEvent) {
allow_err!(send_sas());
});
} else if ck.value() == ControlKey::LockScreen.value() {
- lock_screen();
+ lock_screen_2();
}
}
- Some(key_event::Union::chr(chr)) => {
+ Some(key_event::Union::Chr(chr)) => {
if evt.down {
if en.key_down(get_layout(chr)).is_ok() {
KEYS_DOWN
@@ -708,12 +727,12 @@ fn handle_key_(evt: &KeyEvent) {
.remove(&(chr as u64 + KEY_CHAR_START));
}
}
- Some(key_event::Union::unicode(chr)) => {
+ Some(key_event::Union::Unicode(chr)) => {
if let Ok(chr) = char::try_from(chr) {
en.key_sequence(&chr.to_string());
}
}
- Some(key_event::Union::seq(ref seq)) => {
+ Some(key_event::Union::Seq(ref seq)) => {
en.key_sequence(&seq);
}
_ => {}
@@ -729,6 +748,11 @@ fn handle_key_(evt: &KeyEvent) {
}
}
+#[tokio::main(flavor = "current_thread")]
+async fn lock_screen_2() {
+ lock_screen().await;
+}
+
#[tokio::main(flavor = "current_thread")]
async fn send_sas() -> ResultType<()> {
let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?;
diff --git a/src/server/uinput.rs b/src/server/uinput.rs
new file mode 100644
index 000000000..7a6d47cff
--- /dev/null
+++ b/src/server/uinput.rs
@@ -0,0 +1,651 @@
+use crate::ipc::{self, new_listener, Connection, Data, DataKeyboard, DataMouse};
+use enigo::{Key, KeyboardControllable, MouseButton, MouseControllable};
+use evdev::{
+ uinput::{VirtualDevice, VirtualDeviceBuilder},
+ AttributeSet, EventType, InputEvent,
+};
+use hbb_common::{allow_err, bail, log, tokio, ResultType};
+
+static IPC_CONN_TIMEOUT: u64 = 1000;
+static IPC_REQUEST_TIMEOUT: u64 = 1000;
+static IPC_POSTFIX_KEYBOARD: &str = "_uinput_keyboard";
+static IPC_POSTFIX_MOUSE: &str = "_uinput_mouse";
+static IPC_POSTFIX_CONTROL: &str = "_uinput_control";
+
+pub mod client {
+ use super::*;
+
+ pub struct UInputKeyboard {
+ conn: Connection,
+ }
+
+ impl UInputKeyboard {
+ pub async fn new() -> ResultType {
+ let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_KEYBOARD).await?;
+ Ok(Self { conn })
+ }
+
+ #[tokio::main(flavor = "current_thread")]
+ async fn send(&mut self, data: Data) -> ResultType<()> {
+ self.conn.send(&data).await
+ }
+
+ #[tokio::main(flavor = "current_thread")]
+ async fn send_get_key_state(&mut self, data: Data) -> ResultType {
+ self.conn.send(&data).await?;
+
+ match self.conn.next_timeout(IPC_REQUEST_TIMEOUT).await {
+ Ok(Some(Data::KeyboardResponse(ipc::DataKeyboardResponse::GetKeyState(state)))) => {
+ Ok(state)
+ }
+ Ok(Some(resp)) => {
+ // FATAL error!!!
+ bail!(
+ "FATAL error, wait keyboard result other response: {:?}",
+ &resp
+ );
+ }
+ Ok(None) => {
+ // FATAL error!!!
+ // Maybe wait later
+ bail!("FATAL error, wait keyboard result, receive None",);
+ }
+ Err(e) => {
+ // FATAL error!!!
+ bail!(
+ "FATAL error, wait keyboard result timeout {}, {}",
+ &e,
+ IPC_REQUEST_TIMEOUT
+ );
+ }
+ }
+ }
+ }
+
+ impl KeyboardControllable for UInputKeyboard {
+ fn get_key_state(&mut self, key: Key) -> bool {
+ match self.send_get_key_state(Data::Keyboard(DataKeyboard::GetKeyState(key))) {
+ Ok(state) => state,
+ Err(e) => {
+ // unreachable!()
+ log::error!("Failed to get key state {}", &e);
+ false
+ }
+ }
+ }
+
+ fn key_sequence(&mut self, sequence: &str) {
+ allow_err!(self.send(Data::Keyboard(DataKeyboard::Sequence(sequence.to_string()))));
+ }
+
+ // TODO: handle error???
+ fn key_down(&mut self, key: Key) -> enigo::ResultType {
+ allow_err!(self.send(Data::Keyboard(DataKeyboard::KeyDown(key))));
+ Ok(())
+ }
+ fn key_up(&mut self, key: Key) {
+ allow_err!(self.send(Data::Keyboard(DataKeyboard::KeyUp(key))));
+ }
+ fn key_click(&mut self, key: Key) {
+ allow_err!(self.send(Data::Keyboard(DataKeyboard::KeyClick(key))));
+ }
+ }
+
+ pub struct UInputMouse {
+ conn: Connection,
+ }
+
+ impl UInputMouse {
+ pub async fn new() -> ResultType {
+ let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_MOUSE).await?;
+ Ok(Self { conn })
+ }
+
+ #[tokio::main(flavor = "current_thread")]
+ async fn send(&mut self, data: Data) -> ResultType<()> {
+ self.conn.send(&data).await
+ }
+ }
+
+ impl MouseControllable for UInputMouse {
+ fn mouse_move_to(&mut self, x: i32, y: i32) {
+ allow_err!(self.send(Data::Mouse(DataMouse::MoveTo(x, y))));
+ }
+ fn mouse_move_relative(&mut self, x: i32, y: i32) {
+ allow_err!(self.send(Data::Mouse(DataMouse::MoveRelative(x, y))));
+ }
+ // TODO: handle error???
+ fn mouse_down(&mut self, button: MouseButton) -> enigo::ResultType {
+ allow_err!(self.send(Data::Mouse(DataMouse::Down(button))));
+ Ok(())
+ }
+ fn mouse_up(&mut self, button: MouseButton) {
+ allow_err!(self.send(Data::Mouse(DataMouse::Up(button))));
+ }
+ fn mouse_click(&mut self, button: MouseButton) {
+ allow_err!(self.send(Data::Mouse(DataMouse::Click(button))));
+ }
+ fn mouse_scroll_x(&mut self, length: i32) {
+ allow_err!(self.send(Data::Mouse(DataMouse::ScrollX(length))));
+ }
+ fn mouse_scroll_y(&mut self, length: i32) {
+ allow_err!(self.send(Data::Mouse(DataMouse::ScrollY(length))));
+ }
+ }
+
+ pub async fn set_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> {
+ let mut conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_CONTROL).await?;
+ conn.send(&Data::Control(ipc::DataControl::Resolution {
+ minx,
+ maxx,
+ miny,
+ maxy,
+ }))
+ .await?;
+ let _ = conn.next().await?;
+ Ok(())
+ }
+}
+
+pub mod service {
+ use super::*;
+ use hbb_common::lazy_static;
+ use mouce::MouseActions;
+ use std::{collections::HashMap, sync::Mutex};
+
+ lazy_static::lazy_static! {
+ static ref KEY_MAP: HashMap = HashMap::from(
+ [
+ (enigo::Key::Alt, evdev::Key::KEY_LEFTALT),
+ (enigo::Key::Backspace, evdev::Key::KEY_BACKSPACE),
+ (enigo::Key::CapsLock, evdev::Key::KEY_CAPSLOCK),
+ (enigo::Key::Control, evdev::Key::KEY_LEFTCTRL),
+ (enigo::Key::Delete, evdev::Key::KEY_DELETE),
+ (enigo::Key::DownArrow, evdev::Key::KEY_DOWN),
+ (enigo::Key::End, evdev::Key::KEY_END),
+ (enigo::Key::Escape, evdev::Key::KEY_ESC),
+ (enigo::Key::F1, evdev::Key::KEY_F1),
+ (enigo::Key::F10, evdev::Key::KEY_F10),
+ (enigo::Key::F11, evdev::Key::KEY_F11),
+ (enigo::Key::F12, evdev::Key::KEY_F12),
+ (enigo::Key::F2, evdev::Key::KEY_F2),
+ (enigo::Key::F3, evdev::Key::KEY_F3),
+ (enigo::Key::F4, evdev::Key::KEY_F4),
+ (enigo::Key::F5, evdev::Key::KEY_F5),
+ (enigo::Key::F6, evdev::Key::KEY_F6),
+ (enigo::Key::F7, evdev::Key::KEY_F7),
+ (enigo::Key::F8, evdev::Key::KEY_F8),
+ (enigo::Key::F9, evdev::Key::KEY_F9),
+ (enigo::Key::Home, evdev::Key::KEY_HOME),
+ (enigo::Key::LeftArrow, evdev::Key::KEY_LEFT),
+ (enigo::Key::Meta, evdev::Key::KEY_LEFTMETA),
+ (enigo::Key::Option, evdev::Key::KEY_OPTION),
+ (enigo::Key::PageDown, evdev::Key::KEY_PAGEDOWN),
+ (enigo::Key::PageUp, evdev::Key::KEY_PAGEUP),
+ (enigo::Key::Return, evdev::Key::KEY_ENTER),
+ (enigo::Key::RightArrow, evdev::Key::KEY_RIGHT),
+ (enigo::Key::Shift, evdev::Key::KEY_LEFTSHIFT),
+ (enigo::Key::Space, evdev::Key::KEY_SPACE),
+ (enigo::Key::Tab, evdev::Key::KEY_TAB),
+ (enigo::Key::UpArrow, evdev::Key::KEY_UP),
+ (enigo::Key::Numpad0, evdev::Key::KEY_KP0), // check if correct?
+ (enigo::Key::Numpad1, evdev::Key::KEY_KP1),
+ (enigo::Key::Numpad2, evdev::Key::KEY_KP2),
+ (enigo::Key::Numpad3, evdev::Key::KEY_KP3),
+ (enigo::Key::Numpad4, evdev::Key::KEY_KP4),
+ (enigo::Key::Numpad5, evdev::Key::KEY_KP5),
+ (enigo::Key::Numpad6, evdev::Key::KEY_KP6),
+ (enigo::Key::Numpad7, evdev::Key::KEY_KP7),
+ (enigo::Key::Numpad8, evdev::Key::KEY_KP8),
+ (enigo::Key::Numpad9, evdev::Key::KEY_KP9),
+ (enigo::Key::Cancel, evdev::Key::KEY_CANCEL),
+ (enigo::Key::Clear, evdev::Key::KEY_CLEAR),
+ (enigo::Key::Alt, evdev::Key::KEY_LEFTALT),
+ (enigo::Key::Pause, evdev::Key::KEY_PAUSE),
+ (enigo::Key::Kana, evdev::Key::KEY_KATAKANA), // check if correct?
+ (enigo::Key::Hangul, evdev::Key::KEY_HANGEUL), // check if correct?
+ // (enigo::Key::Junja, evdev::Key::KEY_JUNJA), // map?
+ // (enigo::Key::Final, evdev::Key::KEY_FINAL), // map?
+ (enigo::Key::Hanja, evdev::Key::KEY_HANJA),
+ // (enigo::Key::Kanji, evdev::Key::KEY_KANJI), // map?
+ // (enigo::Key::Convert, evdev::Key::KEY_CONVERT),
+ (enigo::Key::Select, evdev::Key::KEY_SELECT),
+ (enigo::Key::Print, evdev::Key::KEY_PRINT),
+ // (enigo::Key::Execute, evdev::Key::KEY_EXECUTE),
+ // (enigo::Key::Snapshot, evdev::Key::KEY_SNAPSHOT),
+ (enigo::Key::Insert, evdev::Key::KEY_INSERT),
+ (enigo::Key::Help, evdev::Key::KEY_HELP),
+ (enigo::Key::Sleep, evdev::Key::KEY_SLEEP),
+ // (enigo::Key::Separator, evdev::Key::KEY_SEPARATOR),
+ (enigo::Key::Scroll, evdev::Key::KEY_SCROLLLOCK),
+ (enigo::Key::NumLock, evdev::Key::KEY_NUMLOCK),
+ (enigo::Key::RWin, evdev::Key::KEY_RIGHTMETA),
+ (enigo::Key::Apps, evdev::Key::KEY_CONTEXT_MENU),
+ (enigo::Key::Multiply, evdev::Key::KEY_KPASTERISK),
+ (enigo::Key::Add, evdev::Key::KEY_KPPLUS),
+ (enigo::Key::Subtract, evdev::Key::KEY_KPMINUS),
+ (enigo::Key::Decimal, evdev::Key::KEY_KPCOMMA), // KEY_KPDOT and KEY_KPCOMMA are exchanged?
+ (enigo::Key::Divide, evdev::Key::KEY_KPSLASH),
+ (enigo::Key::Equals, evdev::Key::KEY_KPEQUAL),
+ (enigo::Key::NumpadEnter, evdev::Key::KEY_KPENTER),
+ (enigo::Key::RightAlt, evdev::Key::KEY_RIGHTALT),
+ (enigo::Key::RightControl, evdev::Key::KEY_RIGHTCTRL),
+ (enigo::Key::RightShift, evdev::Key::KEY_RIGHTSHIFT),
+ ]);
+
+ static ref KEY_MAP_LAYOUT: HashMap = HashMap::from(
+ [
+ ('a', evdev::Key::KEY_A),
+ ('b', evdev::Key::KEY_B),
+ ('c', evdev::Key::KEY_C),
+ ('d', evdev::Key::KEY_D),
+ ('e', evdev::Key::KEY_E),
+ ('f', evdev::Key::KEY_F),
+ ('g', evdev::Key::KEY_G),
+ ('h', evdev::Key::KEY_H),
+ ('i', evdev::Key::KEY_I),
+ ('j', evdev::Key::KEY_J),
+ ('k', evdev::Key::KEY_K),
+ ('l', evdev::Key::KEY_L),
+ ('m', evdev::Key::KEY_M),
+ ('n', evdev::Key::KEY_N),
+ ('o', evdev::Key::KEY_O),
+ ('p', evdev::Key::KEY_P),
+ ('q', evdev::Key::KEY_Q),
+ ('r', evdev::Key::KEY_R),
+ ('s', evdev::Key::KEY_S),
+ ('t', evdev::Key::KEY_T),
+ ('u', evdev::Key::KEY_U),
+ ('v', evdev::Key::KEY_V),
+ ('w', evdev::Key::KEY_W),
+ ('x', evdev::Key::KEY_X),
+ ('y', evdev::Key::KEY_Y),
+ ('z', evdev::Key::KEY_Z),
+ ('0', evdev::Key::KEY_0),
+ ('1', evdev::Key::KEY_1),
+ ('2', evdev::Key::KEY_2),
+ ('3', evdev::Key::KEY_3),
+ ('4', evdev::Key::KEY_4),
+ ('5', evdev::Key::KEY_5),
+ ('6', evdev::Key::KEY_6),
+ ('7', evdev::Key::KEY_7),
+ ('8', evdev::Key::KEY_8),
+ ('9', evdev::Key::KEY_9),
+ ('`', evdev::Key::KEY_GRAVE),
+ ('-', evdev::Key::KEY_MINUS),
+ ('=', evdev::Key::KEY_EQUAL),
+ ('[', evdev::Key::KEY_LEFTBRACE),
+ (']', evdev::Key::KEY_RIGHTBRACE),
+ ('\\', evdev::Key::KEY_BACKSLASH),
+ (',', evdev::Key::KEY_COMMA),
+ ('.', evdev::Key::KEY_DOT),
+ ('/', evdev::Key::KEY_SLASH),
+ (';', evdev::Key::KEY_SEMICOLON),
+ ('\'', evdev::Key::KEY_APOSTROPHE),
+ ]);
+
+ // ((minx, maxx), (miny, maxy))
+ static ref RESOLUTION: Mutex<((i32, i32), (i32, i32))> = Mutex::new(((0, 0), (0, 0)));
+ }
+
+ fn create_uinput_keyboard() -> ResultType {
+ // TODO: ensure keys here
+ let mut keys = AttributeSet::::new();
+ for i in evdev::Key::KEY_ESC.code()..(evdev::Key::BTN_TRIGGER_HAPPY40.code() + 1) {
+ let key = evdev::Key::new(i);
+ if !format!("{:?}", &key).contains("unknown key") {
+ keys.insert(key);
+ }
+ }
+ let mut leds = AttributeSet::::new();
+ leds.insert(evdev::LedType::LED_NUML);
+ leds.insert(evdev::LedType::LED_CAPSL);
+ leds.insert(evdev::LedType::LED_SCROLLL);
+ let mut miscs = AttributeSet::::new();
+ miscs.insert(evdev::MiscType::MSC_SCAN);
+ let keyboard = VirtualDeviceBuilder::new()?
+ .name("RustDesk UInput Keyboard")
+ .with_keys(&keys)?
+ .with_leds(&leds)?
+ .with_miscs(&miscs)?
+ .build()?;
+ Ok(keyboard)
+ }
+
+ fn map_key(key: &enigo::Key) -> ResultType {
+ if let Some(k) = KEY_MAP.get(&key) {
+ log::trace!("mapkey {:?}, get {:?}", &key, &k);
+ return Ok(k.clone());
+ } else {
+ match key {
+ enigo::Key::Layout(c) => {
+ if let Some(k) = KEY_MAP_LAYOUT.get(&c) {
+ log::trace!("mapkey {:?}, get {:?}", &key, k);
+ return Ok(k.clone());
+ }
+ }
+ // enigo::Key::Raw(c) => {
+ // let k = evdev::Key::new(c);
+ // if !format!("{:?}", &k).contains("unknown key") {
+ // return Ok(k.clone());
+ // }
+ // }
+ _ => {}
+ }
+ }
+ bail!("Failed to map key {:?}", &key);
+ }
+
+ async fn ipc_send_data(stream: &mut Connection, data: &Data) {
+ allow_err!(stream.send(data).await);
+ }
+
+ async fn handle_keyboard(
+ stream: &mut Connection,
+ keyboard: &mut VirtualDevice,
+ data: &DataKeyboard,
+ ) {
+ log::trace!("handle_keyboard {:?}", &data);
+ match data {
+ DataKeyboard::Sequence(_seq) => {
+ // ignore
+ }
+ DataKeyboard::KeyDown(key) => {
+ if let Ok(k) = map_key(key) {
+ let down_event = InputEvent::new(EventType::KEY, k.code(), 1);
+ allow_err!(keyboard.emit(&[down_event]));
+ }
+ }
+ DataKeyboard::KeyUp(key) => {
+ if let Ok(k) = map_key(key) {
+ let up_event = InputEvent::new(EventType::KEY, k.code(), 0);
+ allow_err!(keyboard.emit(&[up_event]));
+ }
+ }
+ DataKeyboard::KeyClick(key) => {
+ if let Ok(k) = map_key(key) {
+ let down_event = InputEvent::new(EventType::KEY, k.code(), 1);
+ let up_event = InputEvent::new(EventType::KEY, k.code(), 0);
+ allow_err!(keyboard.emit(&[down_event, up_event]));
+ }
+ }
+ DataKeyboard::GetKeyState(key) => {
+ let key_state = if enigo::Key::CapsLock == *key {
+ match keyboard.get_led_state() {
+ Ok(leds) => leds.contains(evdev::LedType::LED_CAPSL),
+ Err(_e) => {
+ // log::debug!("Failed to get led state {}", &_e);
+ false
+ }
+ }
+ } else {
+ match keyboard.get_key_state() {
+ Ok(keys) => match key {
+ enigo::Key::Shift => {
+ keys.contains(evdev::Key::KEY_LEFTSHIFT)
+ || keys.contains(evdev::Key::KEY_RIGHTSHIFT)
+ }
+ enigo::Key::Control => {
+ keys.contains(evdev::Key::KEY_LEFTCTRL)
+ || keys.contains(evdev::Key::KEY_RIGHTCTRL)
+ }
+ enigo::Key::Alt => {
+ keys.contains(evdev::Key::KEY_LEFTALT)
+ || keys.contains(evdev::Key::KEY_RIGHTALT)
+ }
+ enigo::Key::NumLock => keys.contains(evdev::Key::KEY_NUMLOCK),
+ enigo::Key::Meta => {
+ keys.contains(evdev::Key::KEY_LEFTMETA)
+ || keys.contains(evdev::Key::KEY_RIGHTMETA)
+ }
+ _ => false,
+ },
+ Err(_e) => {
+ // log::debug!("Failed to get key state: {}", &_e);
+ false
+ }
+ }
+ };
+ ipc_send_data(
+ stream,
+ &Data::KeyboardResponse(ipc::DataKeyboardResponse::GetKeyState(key_state)),
+ )
+ .await;
+ }
+ }
+ }
+
+ fn handle_mouse(mouse: &mut mouce::nix::UInputMouseManager, data: &DataMouse) {
+ log::trace!("handle_mouse {:?}", &data);
+ match data {
+ DataMouse::MoveTo(x, y) => {
+ allow_err!(mouse.move_to(*x as _, *y as _))
+ }
+ DataMouse::MoveRelative(x, y) => {
+ allow_err!(mouse.move_relative(*x, *y))
+ }
+ DataMouse::Down(button) => {
+ let btn = match button {
+ enigo::MouseButton::Left => mouce::common::MouseButton::Left,
+ enigo::MouseButton::Middle => mouce::common::MouseButton::Middle,
+ enigo::MouseButton::Right => mouce::common::MouseButton::Right,
+ _ => {
+ return;
+ }
+ };
+ allow_err!(mouse.press_button(&btn))
+ }
+ DataMouse::Up(button) => {
+ let btn = match button {
+ enigo::MouseButton::Left => mouce::common::MouseButton::Left,
+ enigo::MouseButton::Middle => mouce::common::MouseButton::Middle,
+ enigo::MouseButton::Right => mouce::common::MouseButton::Right,
+ _ => {
+ return;
+ }
+ };
+ allow_err!(mouse.release_button(&btn))
+ }
+ DataMouse::Click(button) => {
+ let btn = match button {
+ enigo::MouseButton::Left => mouce::common::MouseButton::Left,
+ enigo::MouseButton::Middle => mouce::common::MouseButton::Middle,
+ enigo::MouseButton::Right => mouce::common::MouseButton::Right,
+ _ => {
+ return;
+ }
+ };
+ allow_err!(mouse.click_button(&btn))
+ }
+ DataMouse::ScrollX(_length) => {
+ // TODO: not supported for now
+ }
+ DataMouse::ScrollY(length) => {
+ let mut length = *length;
+
+ let scroll = if length < 0 {
+ mouce::common::ScrollDirection::Up
+ } else {
+ mouce::common::ScrollDirection::Down
+ };
+
+ if length < 0 {
+ length = -length;
+ }
+
+ for _ in 0..length {
+ allow_err!(mouse.scroll_wheel(&scroll))
+ }
+ }
+ }
+ }
+
+ fn spawn_keyboard_handler(mut stream: Connection) {
+ tokio::spawn(async move {
+ let mut keyboard = match create_uinput_keyboard() {
+ Ok(keyboard) => keyboard,
+ Err(e) => {
+ log::error!("Failed to create keyboard {}", e);
+ return;
+ }
+ };
+ loop {
+ tokio::select! {
+ res = stream.next() => {
+ match res {
+ Err(err) => {
+ log::info!("UInput keyboard ipc connection closed: {}", err);
+ break;
+ }
+ Ok(Some(data)) => {
+ match data {
+ Data::Keyboard(data) => {
+ handle_keyboard(&mut stream, &mut keyboard, &data).await;
+ }
+ _ => {
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+ });
+ }
+
+ fn spawn_mouse_handler(mut stream: ipc::Connection) {
+ let resolution = RESOLUTION.lock().unwrap();
+ if resolution.0 .0 == resolution.0 .1 || resolution.1 .0 == resolution.1 .1 {
+ return;
+ }
+ let rng_x = resolution.0.clone();
+ let rng_y = resolution.1.clone();
+ tokio::spawn(async move {
+ log::info!(
+ "Create uinput mouce with rng_x: ({}, {}), rng_y: ({}, {})",
+ rng_x.0,
+ rng_x.1,
+ rng_y.0,
+ rng_y.1
+ );
+ let mut mouse = match mouce::Mouse::new_uinput(rng_x, rng_y) {
+ Ok(mouse) => mouse,
+ Err(e) => {
+ log::error!("Failed to create mouse, {}", e);
+ return;
+ }
+ };
+ loop {
+ tokio::select! {
+ res = stream.next() => {
+ match res {
+ Err(err) => {
+ log::info!("UInput mouse ipc connection closed: {}", err);
+ break;
+ }
+ Ok(Some(data)) => {
+ match data {
+ Data::Mouse(data) => {
+ handle_mouse(&mut mouse, &data);
+ }
+ _ => {
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+ });
+ }
+
+ fn spawn_controller_handler(mut stream: ipc::Connection) {
+ tokio::spawn(async move {
+ loop {
+ tokio::select! {
+ res = stream.next() => {
+ match res {
+ Err(_err) => {
+ // log::info!("UInput controller ipc connection closed: {}", err);
+ break;
+ }
+ Ok(Some(data)) => {
+ match data {
+ Data::Control(data) => match data {
+ ipc::DataControl::Resolution{
+ minx,
+ maxx,
+ miny,
+ maxy,
+ } => {
+ *RESOLUTION.lock().unwrap() = ((minx, maxx), (miny, maxy));
+ allow_err!(stream.send(&Data::Empty).await);
+ }
+ }
+ _ => {
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /// Start uinput service.
+ async fn start_service(postfix: &str, handler: F) {
+ match new_listener(postfix).await {
+ Ok(mut incoming) => {
+ while let Some(result) = incoming.next().await {
+ match result {
+ Ok(stream) => {
+ log::debug!("Got new connection of uinput ipc {}", postfix);
+ handler(Connection::new(stream));
+ }
+ Err(err) => {
+ log::error!("Couldn't get uinput mouse client: {:?}", err);
+ }
+ }
+ }
+ }
+ Err(err) => {
+ log::error!("Failed to start uinput mouse ipc service: {}", err);
+ }
+ }
+ }
+
+ /// Start uinput keyboard service.
+ #[tokio::main(flavor = "current_thread")]
+ pub async fn start_service_keyboard() {
+ log::info!("start uinput keyboard service");
+ start_service(IPC_POSTFIX_KEYBOARD, spawn_keyboard_handler).await;
+ }
+
+ /// Start uinput mouse service.
+ #[tokio::main(flavor = "current_thread")]
+ pub async fn start_service_mouse() {
+ log::info!("start uinput mouse service");
+ start_service(IPC_POSTFIX_MOUSE, spawn_mouse_handler).await;
+ }
+
+ /// Start uinput mouse service.
+ #[tokio::main(flavor = "current_thread")]
+ pub async fn start_service_control() {
+ log::info!("start uinput control service");
+ start_service(IPC_POSTFIX_CONTROL, spawn_controller_handler).await;
+ }
+
+ pub fn stop_service_keyboard() {
+ log::info!("stop uinput keyboard service");
+ }
+ pub fn stop_service_mouse() {
+ log::info!("stop uinput mouse service");
+ }
+ pub fn stop_service_control() {
+ log::info!("stop uinput control service");
+ }
+}
diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs
new file mode 100644
index 000000000..b0e06bc03
--- /dev/null
+++ b/src/server/video_qos.rs
@@ -0,0 +1,218 @@
+use super::*;
+use std::time::Duration;
+const FPS: u8 = 30;
+trait Percent {
+ fn as_percent(&self) -> u32;
+}
+
+impl Percent for ImageQuality {
+ fn as_percent(&self) -> u32 {
+ match self {
+ ImageQuality::NotSet => 0,
+ ImageQuality::Low => 50,
+ ImageQuality::Balanced => 66,
+ ImageQuality::Best => 100,
+ }
+ }
+}
+
+pub struct VideoQoS {
+ width: u32,
+ height: u32,
+ user_image_quality: u32,
+ current_image_quality: u32,
+ enable_abr: bool,
+ pub current_delay: u32,
+ pub fps: u8, // abr
+ pub target_bitrate: u32, // abr
+ updated: bool,
+ state: DelayState,
+ debounce_count: u32,
+}
+
+#[derive(PartialEq, Debug)]
+enum DelayState {
+ Normal = 0,
+ LowDelay = 200,
+ HighDelay = 500,
+ Broken = 1000,
+}
+
+impl DelayState {
+ fn from_delay(delay: u32) -> Self {
+ if delay > DelayState::Broken as u32 {
+ DelayState::Broken
+ } else if delay > DelayState::HighDelay as u32 {
+ DelayState::HighDelay
+ } else if delay > DelayState::LowDelay as u32 {
+ DelayState::LowDelay
+ } else {
+ DelayState::Normal
+ }
+ }
+}
+
+impl Default for VideoQoS {
+ fn default() -> Self {
+ VideoQoS {
+ fps: FPS,
+ user_image_quality: ImageQuality::Balanced.as_percent(),
+ current_image_quality: ImageQuality::Balanced.as_percent(),
+ enable_abr: false,
+ width: 0,
+ height: 0,
+ current_delay: 0,
+ target_bitrate: 0,
+ updated: false,
+ state: DelayState::Normal,
+ debounce_count: 0,
+ }
+ }
+}
+
+impl VideoQoS {
+ pub fn set_size(&mut self, width: u32, height: u32) {
+ if width == 0 || height == 0 {
+ return;
+ }
+ self.width = width;
+ self.height = height;
+ }
+
+ pub fn spf(&mut self) -> Duration {
+ if self.fps <= 0 {
+ self.fps = FPS;
+ }
+ Duration::from_secs_f32(1. / (self.fps as f32))
+ }
+
+ // update_network_delay periodically
+ // decrease the bitrate when the delay gets bigger
+ pub fn update_network_delay(&mut self, delay: u32) {
+ if self.current_delay.eq(&0) {
+ self.current_delay = delay;
+ return;
+ }
+
+ self.current_delay = delay / 2 + self.current_delay / 2;
+ log::trace!(
+ "VideoQoS update_network_delay:{}, {}, state:{:?}",
+ self.current_delay,
+ delay,
+ self.state,
+ );
+
+ // ABR
+ if !self.enable_abr {
+ return;
+ }
+ let current_state = DelayState::from_delay(self.current_delay);
+ if current_state != self.state && self.debounce_count > 5 {
+ log::debug!(
+ "VideoQoS state changed:{:?} -> {:?}",
+ self.state,
+ current_state
+ );
+ self.state = current_state;
+ self.debounce_count = 0;
+ self.refresh_quality();
+ } else {
+ self.debounce_count += 1;
+ }
+ }
+
+ fn refresh_quality(&mut self) {
+ match self.state {
+ DelayState::Normal => {
+ self.fps = FPS;
+ self.current_image_quality = self.user_image_quality;
+ }
+ DelayState::LowDelay => {
+ self.fps = FPS;
+ self.current_image_quality = std::cmp::min(self.user_image_quality, 50);
+ }
+ DelayState::HighDelay => {
+ self.fps = FPS / 2;
+ self.current_image_quality = std::cmp::min(self.user_image_quality, 25);
+ }
+ DelayState::Broken => {
+ self.fps = FPS / 4;
+ self.current_image_quality = 10;
+ }
+ }
+ let _ = self.generate_bitrate().ok();
+ self.updated = true;
+ }
+
+ // handle image_quality change from peer
+ pub fn update_image_quality(&mut self, image_quality: i32) {
+ let image_quality = Self::convert_quality(image_quality) as _;
+ if self.current_image_quality != image_quality {
+ self.current_image_quality = image_quality;
+ let _ = self.generate_bitrate().ok();
+ self.updated = true;
+ }
+
+ self.user_image_quality = self.current_image_quality;
+ }
+
+ pub fn generate_bitrate(&mut self) -> ResultType {
+ // https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/
+ if self.width == 0 || self.height == 0 {
+ bail!("Fail to generate_bitrate, width or height is not set");
+ }
+ if self.current_image_quality == 0 {
+ self.current_image_quality = ImageQuality::Balanced.as_percent();
+ }
+
+ let base_bitrate = ((self.width * self.height) / 800) as u32;
+
+ #[cfg(target_os = "android")]
+ {
+ // fix when andorid screen shrinks
+ let fix = scrap::Display::fix_quality() as u32;
+ log::debug!("Android screen, fix quality:{}", fix);
+ let base_bitrate = base_bitrate * fix;
+ self.target_bitrate = base_bitrate * self.current_image_quality / 100;
+ Ok(self.target_bitrate)
+ }
+ #[cfg(not(target_os = "android"))]
+ {
+ self.target_bitrate = base_bitrate * self.current_image_quality / 100;
+ Ok(self.target_bitrate)
+ }
+ }
+
+ pub fn check_if_updated(&mut self) -> bool {
+ if self.updated {
+ self.updated = false;
+ return true;
+ }
+ return false;
+ }
+
+ pub fn reset(&mut self) {
+ *self = Default::default();
+ }
+
+ pub fn check_abr_config(&mut self) -> bool {
+ self.enable_abr = if let Some(v) = Config2::get().options.get("enable-abr") {
+ v != "N"
+ } else {
+ true // default is true
+ };
+ self.enable_abr
+ }
+
+ pub fn convert_quality(q: i32) -> i32 {
+ if q == ImageQuality::Balanced.value() {
+ 100 * 2 / 3
+ } else if q == ImageQuality::Low.value() {
+ 100 / 2
+ } else if q == ImageQuality::Best.value() {
+ 100
+ } else {
+ (q >> 8 & 0xFF) * 2
+ }
+ }
+}
diff --git a/src/server/video_service.rs b/src/server/video_service.rs
index b486cd311..b22418398 100644
--- a/src/server/video_service.rs
+++ b/src/server/video_service.rs
@@ -18,15 +18,20 @@
// to-do:
// https://slhck.info/video/2017/03/01/rate-control.html
-use super::*;
+use super::{video_qos::VideoQoS, *};
use hbb_common::tokio::sync::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
Mutex as TokioMutex,
};
-use scrap::{Capturer, Config, Display, EncodeFrame, Encoder, Frame, VideoCodecId, STRIDE_ALIGN};
+use scrap::{
+ codec::{Encoder, EncoderCfg, HwEncoderConfig},
+ vpxcodec::{VpxEncoderConfig, VpxVideoCodecId},
+ Capturer, Display, TraitCapturer,
+};
use std::{
collections::HashSet,
- io::{ErrorKind::WouldBlock, Result},
+ io::ErrorKind::WouldBlock,
+ ops::{Deref, DerefMut},
time::{self, Duration, Instant},
};
#[cfg(windows)]
@@ -38,14 +43,13 @@ lazy_static::lazy_static! {
static ref CURRENT_DISPLAY: Arc> = Arc::new(Mutex::new(usize::MAX));
static ref LAST_ACTIVE: Arc> = Arc::new(Mutex::new(Instant::now()));
static ref SWITCH: Arc> = Default::default();
- static ref TEST_LATENCIES: Arc>> = Default::default();
- static ref IMAGE_QUALITIES: Arc>> = Default::default();
static ref FRAME_FETCHED_NOTIFIER: (UnboundedSender<(i32, Option)>, Arc)>>>) = {
let (tx, rx) = unbounded_channel();
(tx, Arc::new(TokioMutex::new(rx)))
};
static ref PRIVACY_MODE_CONN_ID: Mutex = Mutex::new(0);
static ref IS_CAPTURER_MAGNIFIER_SUPPORTED: bool = is_capturer_mag_supported();
+ pub static ref VIDEO_QOS: Arc> = Default::default();
}
fn is_capturer_mag_supported() -> bool {
@@ -124,46 +128,6 @@ impl VideoFrameController {
}
}
-trait TraitCapturer {
- fn frame<'a>(&'a mut self, timeout_ms: u32) -> Result>;
-
- #[cfg(windows)]
- fn is_gdi(&self) -> bool;
- #[cfg(windows)]
- fn set_gdi(&mut self) -> bool;
-}
-
-impl TraitCapturer for Capturer {
- fn frame<'a>(&'a mut self, timeout_ms: u32) -> Result> {
- self.frame(timeout_ms)
- }
-
- #[cfg(windows)]
- fn is_gdi(&self) -> bool {
- self.is_gdi()
- }
-
- #[cfg(windows)]
- fn set_gdi(&mut self) -> bool {
- self.set_gdi()
- }
-}
-
-#[cfg(windows)]
-impl TraitCapturer for scrap::CapturerMag {
- fn frame<'a>(&'a mut self, _timeout_ms: u32) -> Result> {
- self.frame(_timeout_ms)
- }
-
- fn is_gdi(&self) -> bool {
- false
- }
-
- fn set_gdi(&mut self) -> bool {
- false
- }
-}
-
pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true);
sp.run(run);
@@ -176,6 +140,14 @@ fn check_display_changed(
last_width: usize,
last_hegiht: usize,
) -> bool {
+ #[cfg(target_os = "linux")]
+ {
+ // wayland do not support changing display for now
+ if !scrap::is_x11() {
+ return false;
+ }
+ }
+
let displays = match try_get_displays() {
Ok(d) => d,
_ => return false,
@@ -201,9 +173,11 @@ fn check_display_changed(
}
// Capturer object is expensive, avoiding to create it frequently.
-fn create_capturer(privacy_mode_id: i32, display: Display) -> ResultType> {
- let use_yuv = true;
-
+fn create_capturer(
+ privacy_mode_id: i32,
+ display: Display,
+ use_yuv: bool,
+) -> ResultType> {
#[cfg(not(windows))]
let c: Option> = None;
#[cfg(windows)]
@@ -288,11 +262,12 @@ fn ensure_close_virtual_device() -> ResultType<()> {
Ok(())
}
+// This function works on privacy mode. Windows only for now.
pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> bool {
let test_begin = Instant::now();
while test_begin.elapsed().as_millis() < timeout_millis as _ {
if let Ok((_, _, display)) = get_current_display() {
- if let Ok(_) = create_capturer(privacy_mode_id, display) {
+ if let Ok(_) = create_capturer(privacy_mode_id, display, true) {
return true;
}
}
@@ -316,13 +291,39 @@ fn check_uac_switch(privacy_mode_id: i32, captuerer_privacy_mode_id: i32) -> Res
Ok(())
}
-fn run(sp: GenericService) -> ResultType<()> {
- #[cfg(windows)]
- ensure_close_virtual_device()?;
+pub(super) struct CapturerInfo {
+ pub origin: (i32, i32),
+ pub width: usize,
+ pub height: usize,
+ pub ndisplay: usize,
+ pub current: usize,
+ pub privacy_mode_id: i32,
+ pub _captuerer_privacy_mode_id: i32,
+ pub capturer: Box,
+}
+
+impl Deref for CapturerInfo {
+ type Target = Box;
+
+ fn deref(&self) -> &Self::Target {
+ &self.capturer
+ }
+}
+
+impl DerefMut for CapturerInfo {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.capturer
+ }
+}
+
+fn get_capturer(use_yuv: bool) -> ResultType {
+ #[cfg(target_os = "linux")]
+ {
+ if !scrap::is_x11() {
+ return super::wayland::get_capturer();
+ }
+ }
- let fps = 30;
- let wait = 1000 / fps;
- let spf = time::Duration::from_secs_f32(1. / (fps as f32));
let (ndisplay, current, display) = get_current_display()?;
let (origin, width, height) = (display.origin(), display.width(), display.height());
log::debug!(
@@ -342,8 +343,10 @@ fn run(sp: GenericService) -> ResultType<()> {
#[cfg(windows)]
let mut captuerer_privacy_mode_id = privacy_mode_id;
#[cfg(windows)]
- if crate::ui::win_privacy::is_process_consent_running()? {
- captuerer_privacy_mode_id = 0;
+ if captuerer_privacy_mode_id != 0 {
+ if crate::ui::win_privacy::is_process_consent_running()? {
+ captuerer_privacy_mode_id = 0;
+ }
}
log::debug!(
"Try create capturer with captuerer privacy mode id {}",
@@ -355,36 +358,66 @@ fn run(sp: GenericService) -> ResultType<()> {
} else {
log::info!("In privacy mode, the peer side cannot watch the screen");
}
- let mut c = create_capturer(captuerer_privacy_mode_id, display)?;
+ let capturer = create_capturer(captuerer_privacy_mode_id, display, use_yuv)?;
+ Ok(CapturerInfo {
+ origin,
+ width,
+ height,
+ ndisplay,
+ current,
+ privacy_mode_id,
+ _captuerer_privacy_mode_id: captuerer_privacy_mode_id,
+ capturer,
+ })
+}
- let q = get_image_quality();
- let (bitrate, rc_min_quantizer, rc_max_quantizer, speed) = get_quality(width, height, q);
- log::info!("bitrate={}, rc_min_quantizer={}", bitrate, rc_min_quantizer);
- let cfg = Config {
- width: width as _,
- height: height as _,
- timebase: [1, 1000], // Output timestamp precision
- bitrate,
- codec: VideoCodecId::VP9,
- rc_min_quantizer,
- rc_max_quantizer,
- speed,
+fn run(sp: GenericService) -> ResultType<()> {
+ #[cfg(windows)]
+ ensure_close_virtual_device()?;
+
+ let mut c = get_capturer(true)?;
+
+ let mut video_qos = VIDEO_QOS.lock().unwrap();
+ video_qos.set_size(c.width as _, c.height as _);
+ let mut spf = video_qos.spf();
+ let bitrate = video_qos.generate_bitrate()?;
+ let abr = video_qos.check_abr_config();
+ drop(video_qos);
+ log::info!("init bitrate={}, abr enabled:{}", bitrate, abr);
+
+ let encoder_cfg = match Encoder::current_hw_encoder_name() {
+ Some(codec_name) => EncoderCfg::HW(HwEncoderConfig {
+ codec_name,
+ width: c.width,
+ height: c.height,
+ bitrate: bitrate as _,
+ }),
+ None => EncoderCfg::VPX(VpxEncoderConfig {
+ width: c.width as _,
+ height: c.height as _,
+ timebase: [1, 1000], // Output timestamp precision
+ bitrate,
+ codec: VpxVideoCodecId::VP9,
+ num_threads: (num_cpus::get() / 2) as _,
+ }),
};
- let mut vpx;
- match Encoder::new(&cfg, (num_cpus::get() / 2) as _) {
- Ok(x) => vpx = x,
+
+ let mut encoder;
+ match Encoder::new(encoder_cfg) {
+ Ok(x) => encoder = x,
Err(err) => bail!("Failed to create encoder: {}", err),
}
+ c.set_use_yuv(encoder.use_yuv());
if *SWITCH.lock().unwrap() {
log::debug!("Broadcasting display switch");
let mut misc = Misc::new();
misc.set_switch_display(SwitchDisplay {
- display: current as _,
- x: origin.0 as _,
- y: origin.1 as _,
- width: width as _,
- height: height as _,
+ display: c.current as _,
+ x: c.origin.0 as _,
+ y: c.origin.1 as _,
+ width: c.width as _,
+ height: c.height as _,
..Default::default()
});
let mut msg_out = Message::new();
@@ -401,21 +434,35 @@ fn run(sp: GenericService) -> ResultType<()> {
let mut try_gdi = 1;
#[cfg(windows)]
log::info!("gdi: {}", c.is_gdi());
+ let codec_name = Encoder::current_hw_encoder_name();
+
while sp.ok() {
#[cfg(windows)]
- check_uac_switch(privacy_mode_id, captuerer_privacy_mode_id)?;
+ check_uac_switch(c.privacy_mode_id, c._captuerer_privacy_mode_id)?;
+
+ let mut video_qos = VIDEO_QOS.lock().unwrap();
+ if video_qos.check_if_updated() {
+ log::debug!(
+ "qos is updated, target_bitrate:{}, fps:{}",
+ video_qos.target_bitrate,
+ video_qos.fps
+ );
+ encoder.set_bitrate(video_qos.target_bitrate).unwrap();
+ spf = video_qos.spf();
+ }
+ drop(video_qos);
if *SWITCH.lock().unwrap() {
bail!("SWITCH");
}
- if current != *CURRENT_DISPLAY.lock().unwrap() {
+ if c.current != *CURRENT_DISPLAY.lock().unwrap() {
*SWITCH.lock().unwrap() = true;
bail!("SWITCH");
}
- check_privacy_mode_changed(&sp, privacy_mode_id)?;
- if get_image_quality() != q {
+ if codec_name != Encoder::current_hw_encoder_name() {
bail!("SWITCH");
}
+ check_privacy_mode_changed(&sp, c.privacy_mode_id)?;
#[cfg(windows)]
{
if crate::platform::windows::desktop_changed() {
@@ -425,7 +472,7 @@ fn run(sp: GenericService) -> ResultType<()> {
let now = time::Instant::now();
if last_check_displays.elapsed().as_millis() > 1000 {
last_check_displays = now;
- if ndisplay != get_display_num() {
+ if c.ndisplay != get_display_num() {
log::info!("Displays changed");
*SWITCH.lock().unwrap() = true;
bail!("SWITCH");
@@ -437,7 +484,7 @@ fn run(sp: GenericService) -> ResultType<()> {
frame_controller.reset();
#[cfg(any(target_os = "android", target_os = "ios"))]
- let res = match (*c).frame(wait as _) {
+ let res = match c.frame(spf) {
Ok(frame) => {
let time = now - start;
let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64;
@@ -448,7 +495,7 @@ fn run(sp: GenericService) -> ResultType<()> {
}
scrap::Frame::RAW(data) => {
if (data.len() != 0) {
- let send_conn_ids = handle_one_frame(&sp, data, ms, &mut vpx)?;
+ let send_conn_ids = handle_one_frame(&sp, data, ms, &mut encoder)?;
frame_controller.set_send(now, send_conn_ids);
}
}
@@ -460,11 +507,11 @@ fn run(sp: GenericService) -> ResultType<()> {
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
- let res = match (*c).frame(wait as _) {
+ let res = match c.frame(spf) {
Ok(frame) => {
let time = now - start;
let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64;
- let send_conn_ids = handle_one_frame(&sp, &frame, ms, &mut vpx)?;
+ let send_conn_ids = handle_one_frame(&sp, &frame, ms, &mut encoder)?;
frame_controller.set_send(now, send_conn_ids);
#[cfg(windows)]
{
@@ -489,7 +536,7 @@ fn run(sp: GenericService) -> ResultType<()> {
}
}
Err(err) => {
- if check_display_changed(ndisplay, current, width, height) {
+ if check_display_changed(c.ndisplay, c.current, c.width, c.height) {
log::info!("Displays changed");
*SWITCH.lock().unwrap() = true;
bail!("SWITCH");
@@ -511,9 +558,9 @@ fn run(sp: GenericService) -> ResultType<()> {
let timeout_millis = 3_000u64;
let wait_begin = Instant::now();
while wait_begin.elapsed().as_millis() < timeout_millis as _ {
- check_privacy_mode_changed(&sp, privacy_mode_id)?;
+ check_privacy_mode_changed(&sp, c.privacy_mode_id)?;
#[cfg(windows)]
- check_uac_switch(privacy_mode_id, captuerer_privacy_mode_id)?;
+ check_uac_switch(c.privacy_mode_id, c._captuerer_privacy_mode_id)?;
frame_controller.try_wait_next(&mut fetched_conn_ids, 300);
// break if all connections have received current frame
if fetched_conn_ids.len() >= frame_controller.send_conn_ids.len() {
@@ -531,13 +578,12 @@ fn run(sp: GenericService) -> ResultType<()> {
Ok(())
}
-#[inline]
fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> ResultType<()> {
let privacy_mode_id_2 = *PRIVACY_MODE_CONN_ID.lock().unwrap();
if privacy_mode_id != privacy_mode_id_2 {
if privacy_mode_id_2 != 0 {
let msg_out = crate::common::make_privacy_mode_msg(
- back_notification::PrivacyModeState::OnByOther,
+ back_notification::PrivacyModeState::PrvOnByOther,
);
sp.send_to_others(msg_out, privacy_mode_id_2);
}
@@ -547,10 +593,11 @@ fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> Resu
}
#[inline]
-fn create_msg(vp9s: Vec) -> Message {
+#[cfg(any(target_os = "android", target_os = "ios"))]
+fn create_msg(vp9s: Vec) -> Message {
let mut msg_out = Message::new();
let mut vf = VideoFrame::new();
- vf.set_vp9s(VP9s {
+ vf.set_vp9s(EncodedVideoFrames {
frames: vp9s.into(),
..Default::default()
});
@@ -559,22 +606,12 @@ fn create_msg(vp9s: Vec) -> Message {
msg_out
}
-#[inline]
-fn create_frame(frame: &EncodeFrame) -> VP9 {
- VP9 {
- data: frame.data.to_vec(),
- key: frame.key,
- pts: frame.pts,
- ..Default::default()
- }
-}
-
#[inline]
fn handle_one_frame(
sp: &GenericService,
frame: &[u8],
ms: i64,
- vpx: &mut Encoder,
+ encoder: &mut Encoder,
) -> ResultType> {
sp.snapshot(|sps| {
// so that new sub and old sub share the same encoder after switch
@@ -585,20 +622,8 @@ fn handle_one_frame(
})?;
let mut send_conn_ids: HashSet = Default::default();
- let mut frames = Vec::new();
- for ref frame in vpx
- .encode(ms, frame, STRIDE_ALIGN)
- .with_context(|| "Failed to encode")?
- {
- frames.push(create_frame(frame));
- }
- for ref frame in vpx.flush().with_context(|| "Failed to flush")? {
- frames.push(create_frame(frame));
- }
-
- // to-do: flush periodically, e.g. 1 second
- if frames.len() > 0 {
- send_conn_ids = sp.send_video_frame(create_msg(frames));
+ if let Ok(msg) = encoder.encode_to_message(frame, ms) {
+ send_conn_ids = sp.send_video_frame(msg);
}
Ok(send_conn_ids)
}
@@ -618,8 +643,8 @@ pub fn handle_one_frame_encoded(
Ok(())
})?;
let mut send_conn_ids: HashSet = Default::default();
- let vp9_frame = VP9 {
- data: frame.to_vec(),
+ let vp9_frame = EncodedVideoFrame {
+ data: frame.to_vec().into(),
key: true,
pts: ms,
..Default::default()
@@ -629,6 +654,17 @@ pub fn handle_one_frame_encoded(
}
fn get_display_num() -> usize {
+ #[cfg(target_os = "linux")]
+ {
+ if !scrap::is_x11() {
+ return if let Ok(n) = super::wayland::get_display_num() {
+ n
+ } else {
+ 0
+ };
+ }
+ }
+
if let Ok(d) = try_get_displays() {
d.len()
} else {
@@ -636,14 +672,10 @@ fn get_display_num() -> usize {
}
}
-pub fn get_displays() -> ResultType<(usize, Vec)> {
- // switch to primary display if long time (30 seconds) no users
- if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 {
- *CURRENT_DISPLAY.lock().unwrap() = usize::MAX;
- }
+pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) {
let mut displays = Vec::new();
let mut primary = 0;
- for (i, d) in try_get_displays()?.iter().enumerate() {
+ for (i, d) in all.iter().enumerate() {
if d.is_primary() {
primary = i;
}
@@ -661,12 +693,26 @@ pub fn get_displays() -> ResultType<(usize, Vec)> {
if *lock >= displays.len() {
*lock = primary
}
- Ok((*lock, displays))
+ (*lock, displays)
}
-pub fn switch_display(i: i32) {
+pub async fn get_displays() -> ResultType<(usize, Vec)> {
+ #[cfg(target_os = "linux")]
+ {
+ if !scrap::is_x11() {
+ return super::wayland::get_displays().await;
+ }
+ }
+ // switch to primary display if long time (30 seconds) no users
+ if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 {
+ *CURRENT_DISPLAY.lock().unwrap() = usize::MAX;
+ }
+ Ok(get_displays_2(&try_get_displays()?))
+}
+
+pub async fn switch_display(i: i32) {
let i = i as usize;
- if let Ok((_, displays)) = get_displays() {
+ if let Ok((_, displays)) = get_displays().await {
if i < displays.len() {
*CURRENT_DISPLAY.lock().unwrap() = i;
}
@@ -680,6 +726,16 @@ pub fn refresh() {
}
fn get_primary() -> usize {
+ #[cfg(target_os = "linux")]
+ {
+ if !scrap::is_x11() {
+ return match super::wayland::get_primary() {
+ Ok(n) => n,
+ Err(_) => 0,
+ };
+ }
+ }
+
if let Ok(all) = try_get_displays() {
for (i, d) in all.iter().enumerate() {
if d.is_primary() {
@@ -690,8 +746,8 @@ fn get_primary() -> usize {
0
}
-pub fn switch_to_primary() {
- switch_display(get_primary() as _);
+pub async fn switch_to_primary() {
+ switch_display(get_primary() as _).await;
}
#[cfg(not(windows))]
@@ -729,16 +785,15 @@ fn try_get_displays() -> ResultType> {
Ok(displays)
}
-fn get_current_display() -> ResultType<(usize, usize, Display)> {
+pub(super) fn get_current_display_2(mut all: Vec) -> ResultType<(usize, usize, Display)> {
let mut current = *CURRENT_DISPLAY.lock().unwrap() as usize;
- let mut displays = try_get_displays()?;
- if displays.len() == 0 {
+ if all.len() == 0 {
bail!("No displays");
}
- let n = displays.len();
+ let n = all.len();
if current >= n {
current = 0;
- for (i, d) in displays.iter().enumerate() {
+ for (i, d) in all.iter().enumerate() {
if d.is_primary() {
current = i;
break;
@@ -746,84 +801,9 @@ fn get_current_display() -> ResultType<(usize, usize, Display)> {
}
*CURRENT_DISPLAY.lock().unwrap() = current;
}
- return Ok((n, current, displays.remove(current)));
+ return Ok((n, current, all.remove(current)));
}
-#[inline]
-fn update_latency(id: i32, latency: i64, latencies: &mut HashMap) {
- if latency <= 0 {
- latencies.remove(&id);
- } else {
- latencies.insert(id, latency);
- }
-}
-
-pub fn update_test_latency(id: i32, latency: i64) {
- update_latency(id, latency, &mut *TEST_LATENCIES.lock().unwrap());
-}
-
-fn convert_quality(q: i32) -> i32 {
- let q = {
- if q == ImageQuality::Balanced.value() {
- (100 * 2 / 3, 12)
- } else if q == ImageQuality::Low.value() {
- (100 / 2, 18)
- } else if q == ImageQuality::Best.value() {
- (100, 12)
- } else {
- let bitrate = q >> 8 & 0xFF;
- let quantizer = q & 0xFF;
- (bitrate * 2, (100 - quantizer) * 36 / 100)
- }
- };
- if q.0 <= 0 {
- 0
- } else {
- q.0 << 8 | q.1
- }
-}
-
-pub fn update_image_quality(id: i32, q: Option) {
- match q {
- Some(q) => {
- let q = convert_quality(q);
- if q > 0 {
- IMAGE_QUALITIES.lock().unwrap().insert(id, q);
- } else {
- IMAGE_QUALITIES.lock().unwrap().remove(&id);
- }
- }
- None => {
- IMAGE_QUALITIES.lock().unwrap().remove(&id);
- }
- }
-}
-
-fn get_image_quality() -> i32 {
- IMAGE_QUALITIES
- .lock()
- .unwrap()
- .values()
- .min()
- .unwrap_or(&convert_quality(ImageQuality::Balanced.value()))
- .clone()
-}
-
-#[inline]
-fn get_quality(w: usize, h: usize, q: i32) -> (u32, u32, u32, i32) {
- // https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/
- let bitrate = q >> 8 & 0xFF;
- let quantizer = q & 0xFF;
- let b = ((w * h) / 1000) as u32;
-
- #[cfg(target_os = "android")]
- {
- // fix when andorid screen shrinks
- let fix = Display::fix_quality() as u32;
- log::debug!("Android screen, fix quality:{}", fix);
- let b = b * fix;
- return (bitrate as u32 * b / 100, quantizer as _, 56, 7);
- }
-
- (bitrate as u32 * b / 100, quantizer as _, 56, 7)
+fn get_current_display() -> ResultType<(usize, usize, Display)> {
+ get_current_display_2(try_get_displays()?)
}
diff --git a/src/server/wayland.rs b/src/server/wayland.rs
new file mode 100644
index 000000000..1ac2c18be
--- /dev/null
+++ b/src/server/wayland.rs
@@ -0,0 +1,189 @@
+use super::*;
+use hbb_common::allow_err;
+use scrap::{Capturer, Display, Frame, TraitCapturer};
+use std::io::Result;
+
+lazy_static::lazy_static! {
+ static ref CAP_DISPLAY_INFO: RwLock = RwLock::new(0);
+}
+
+struct CapturerPtr(*mut Capturer);
+
+impl Clone for CapturerPtr {
+ fn clone(&self) -> Self {
+ Self(self.0)
+ }
+}
+
+impl TraitCapturer for CapturerPtr {
+ fn frame<'a>(&'a mut self, timeout: Duration) -> Result