Merge pull request #1137 from Kingtous/flutter_desktop

refactor: merge master to flutter_desktop
This commit is contained in:
RustDesk 2022-08-01 15:58:56 +08:00 committed by GitHub
commit 90515ea588
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
145 changed files with 8809 additions and 2317 deletions

View File

@ -78,7 +78,7 @@ jobs:
shell: bash shell: bash
run: | run: |
case ${{ matrix.job.target }} in 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 ;; # 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 ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
esac esac

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
.vscode .vscode
.idea .idea
.DS_Store .DS_Store
libsciter-gtk.so
src/ui/inline.rs src/ui/inline.rs
extractor extractor
__pycache__ __pycache__

View File

@ -26,12 +26,13 @@ use_rubato = ["rubato"]
use_dasp = ["dasp"] use_dasp = ["dasp"]
flutter = ["flutter_rust_bridge"] flutter = ["flutter_rust_bridge"]
default = ["use_dasp","flutter"] default = ["use_dasp","flutter"]
hwcodec = ["scrap/hwcodec"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
whoami = "1.2" whoami = "1.2"
scrap = { path = "libs/scrap" } scrap = { path = "libs/scrap", features = ["wayland"] }
hbb_common = { path = "libs/hbb_common" } hbb_common = { path = "libs/hbb_common" }
serde_derive = "1.0" serde_derive = "1.0"
serde = "1.0" serde = "1.0"
@ -51,10 +52,13 @@ samplerate = { version = "0.2", optional = true }
async-trait = "0.1" async-trait = "0.1"
uuid = { version = "1.0", features = ["v4"] } uuid = { version = "1.0", features = ["v4"] }
clap = "3.0" clap = "3.0"
rpassword = "6.0" rpassword = "7.0"
base64 = "0.13" base64 = "0.13"
sysinfo = "0.23" sysinfo = "0.24"
num_cpus = "1.13" 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 } flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true }
[target.'cfg(not(target_os = "linux"))'.dependencies] [target.'cfg(not(target_os = "linux"))'.dependencies]
@ -68,18 +72,18 @@ machine-uid = "0.2"
mac_address = "1.1" mac_address = "1.1"
sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" } sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" }
sys-locale = "0.2" sys-locale = "0.2"
enigo = { path = "libs/enigo" } enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" } clipboard = { path = "libs/clipboard" }
rdev = { git = "https://github.com/open-trade/rdev" } rdev = { git = "https://github.com/open-trade/rdev" }
ctrlc = "3.2" ctrlc = "3.2"
arboard = "2.0" arboard = "2.0"
#minreq = { version = "2.4", features = ["punycode", "https-native"] } #minreq = { version = "2.4", features = ["punycode", "https-native"] }
system_shutdown = "3.0.0"
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
#systray = { git = "https://github.com/open-trade/systray-rs" } #systray = { git = "https://github.com/open-trade/systray-rs" }
trayicon = { version = "0.1", features = ["winit"] } trayicon = { git = "https://github.com/open-trade/trayicon-rs", features = ["winit"] }
# > 0.25 not work with trayicon winit = "0.26"
winit = "0.25"
winapi = { version = "0.3", features = ["winuser"] } winapi = { version = "0.3", features = ["winuser"] }
winreg = "0.10" winreg = "0.10"
windows-service = "0.4" windows-service = "0.4"
@ -99,10 +103,12 @@ psimple = { package = "libpulse-simple-binding", version = "2.25" }
pulse = { package = "libpulse-binding", version = "2.26" } pulse = { package = "libpulse-binding", version = "2.26" }
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" } rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
async-process = "1.3" 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] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11" android_logger = "0.11"
jni = "0.19.0" jni = "0.19"
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" }

8
DEBIAN/postinst Normal file → Executable file
View File

@ -8,16 +8,20 @@ if [ "$1" = configure ]; then
if [ "systemd" == "$INITSYS" ]; then if [ "systemd" == "$INITSYS" ]; then
if [ -e /etc/systemd/system/rustdesk.service ]; 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 fi
version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)') version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)')
parsedVersion=$(echo "${version//./}") parsedVersion=$(echo "${version//./}")
if [[ "$parsedVersion" -gt "360" ]]; then if [[ "$parsedVersion" -gt "360" ]]; then
sudo -H pip3 install pynput sudo -H pip3 install pynput
fi 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 daemon-reload
systemctl enable rustdesk systemctl enable rustdesk
systemctl start 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
fi fi

0
DEBIAN/postrm Normal file → Executable file
View File

7
DEBIAN/preinst Normal file → Executable file
View File

@ -7,6 +7,13 @@ case $1 in
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
if [ "systemd" == "${INITSYS}" ]; then if [ "systemd" == "${INITSYS}" ]; then
service rustdesk stop || true 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 sleep 1
rm -rf /usr/bin/libsciter-gtk.so rm -rf /usr/bin/libsciter-gtk.so
fi fi

9
DEBIAN/prerm Normal file → Executable file
View File

@ -8,7 +8,14 @@ case $1 in
if [ "systemd" == "${INITSYS}" ]; then if [ "systemd" == "${INITSYS}" ]; then
systemctl stop rustdesk || true systemctl stop rustdesk || true
systemctl disable 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 fi
;; ;;
esac esac

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a> <a href="#file-structure">Structure</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b> لغتك الأم, <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> و <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>, README نحن بحاجة إلى مساعدتك لترجمة هذا </b> <b> لغتك الأم, <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> و <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>, README نحن بحاجة إلى مساعدتك لترجمة هذا </b>
</p> </p>

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Struktura</a> <a href="#file-structure">Struktura</a>
<a href="#snapshot">Ukázky</a><br> <a href="#snapshot">Ukázky</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Potřebujeme Vaši pomoc s překláním textů tohoto ČTIMNE, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">uživatelského rozhraní aplikace RustDesk</a> a <a href="https://github.com/rustdesk/doc.rustdesk.com">dokumentace k ní</a> do vašeho jazyka</b> <b>Potřebujeme Vaši pomoc s překláním textů tohoto ČTIMNE, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">uživatelského rozhraní aplikace RustDesk</a> a <a href="https://github.com/rustdesk/doc.rustdesk.com">dokumentace k ní</a> do vašeho jazyka</b>
</p> </p>

View File

@ -5,11 +5,11 @@
<a href="#auf-docker-kompilieren">Docker</a> <a href="#auf-docker-kompilieren">Docker</a>
<a href="#dateistruktur">Dateistruktur</a> <a href="#dateistruktur">Dateistruktur</a>
<a href="#screenshots">Screenshots</a><br> <a href="#screenshots">Screenshots</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren</b> <b>Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren</b>
</p> </p>
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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

View File

@ -5,11 +5,11 @@
<a href="#kiel-kompili-kun-docker">Docker</a> <a href="#kiel-kompili-kun-docker">Docker</a>
<a href="#dosierstrukturo">Strukturo</a> <a href="#dosierstrukturo">Strukturo</a>
<a href="#ekrankopio">Ekrankopio</a><br> <a href="#ekrankopio">Ekrankopio</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Ni bezonas helpon traduki tiun README kaj <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">la interfacon</a> al via denaska lingvo</b> <b>Ni bezonas helpon traduki tiun README kaj <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">la interfacon</a> al via denaska lingvo</b>
</p> </p>
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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

View File

@ -5,7 +5,7 @@
<a href="#como-compilar-con-docker">Docker</a> <a href="#como-compilar-con-docker">Docker</a>
<a href="#estructura-de-archivos">Estructura</a> <a href="#estructura-de-archivos">Estructura</a>
<a href="#captura-de-pantalla">Captura de pantalla</a><br> <a href="#captura-de-pantalla">Captura de pantalla</a><br>
[<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Necesitamos tu ayuda para traducir este README a tu idioma</b> <b>Necesitamos tu ayuda para traducir este README a tu idioma</b>
</p> </p>

View File

@ -5,11 +5,11 @@
<a dir="rtl" href="#نحوه-ساخت-با-داکر">داکر</a> <a dir="rtl" href="#نحوه-ساخت-با-داکر">داکر</a>
<a dir="rtl" href="#ساخت">ساخت</a> <a dir="rtl" href="#ساخت">ساخت</a>
<a dir="rtl" href="#سرورهای-عمومی-رایگان">سرور</a><br> <a dir="rtl" href="#سرورهای-عمومی-رایگان">سرور</a><br>
[<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
&#x202b;<b>برای ترجمه این <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang"> RustDesk UI</a> ،README و <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> به زبان مادری شما به کمکتون نیاز داریم &#x202b;<b>برای ترجمه این <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang"> RustDesk UI</a> ،README و <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> به زبان مادری شما به کمکتون نیاز داریم
</p> </p>
با ما گپ بزنید: [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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

View File

@ -5,11 +5,11 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Rakenne</a> <a href="#file-structure">Rakenne</a>
<a href="#snapshot">Tilannevedos</a><br> <a href="#snapshot">Tilannevedos</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi</b> <b>Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi</b>
</p> </p>
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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

View File

@ -5,11 +5,11 @@
<a href="#comment-construire-avec-docker">Docker</a> - <a href="#comment-construire-avec-docker">Docker</a> -
<a href="#structure-du-projet">Structure</a> - <a href="#structure-du-projet">Structure</a> -
<a href="#images">Images</a><br> <a href="#images">Images</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle</b>. <b>Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle</b>.
</p> </p>
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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

182
README-HU.md Normal file
View File

@ -0,0 +1,182 @@
<p align="center">
<img src="logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#ingyenes-publikus-szerverek">Szerverek</a>
<a href="#építési-pontok">Építés</a>
<a href="#hogyan-éptís-dockerrel">Docker</a>
<a href="#fájl-struktúra">Struktúra</a>
<a href="#képernyőképek">Képernyőképek</a><br>
[<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Kell a segítséged, hogy lefordítsuk ezt a README-t, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">a RustDesk UI-t</a> és a <a href="https://github.com/rustdesk/doc.rustdesk.com">Dokumentációt</a> az anyanyelvedre</b>
</p>
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)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](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 `<OPTIONAL-ARGS>` 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)

View File

@ -5,11 +5,11 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a> <a href="#file-structure">Structure</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Kami membutuhkan bantuan Anda untuk menerjemahkan README ini dan <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ke bahasa asli anda</b> <b>Kami membutuhkan bantuan Anda untuk menerjemahkan README ini dan <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ke bahasa asli anda</b>
</p> </p>
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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

View File

@ -5,11 +5,11 @@
<a href="#come-compilare-con-docker">Docker</a> <a href="#come-compilare-con-docker">Docker</a>
<a href="#struttura-dei-file">Struttura</a> <a href="#struttura-dei-file">Struttura</a>
<a href="#screenshots">Screenshots</a><br> <a href="#screenshots">Screenshots</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Abbiamo bisogno del tuo aiuto per tradurre questo README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> nella tua lingua nativa</b> <b>Abbiamo bisogno del tuo aiuto per tradurre questo README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> nella tua lingua nativa</b>
</p> </p>
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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a> <a href="#file-structure">Structure</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。</b> <b>このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。</b>
</p> </p>

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a> <a href="#file-structure">Structure</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.</b> <b>README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.</b>
</p> </p>

View File

@ -5,11 +5,11 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a> <a href="#file-structure">Structure</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്</b> <b>ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്</b>
</p> </p>
ഞങ്ങളുമായി ചാറ്റ് ചെയ്യുക: [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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

View File

@ -5,11 +5,11 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structuur</a> <a href="#file-structure">Structuur</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>We hebben je hulp nodig om deze README te vertalen naar jouw moedertaal</b> <b>We hebben je hulp nodig om deze README te vertalen naar jouw moedertaal</b>
</p> </p>
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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

View File

@ -5,11 +5,11 @@
<a href="#jak-kompilować-za-pomocą-dockera">Docker</a> <a href="#jak-kompilować-za-pomocą-dockera">Docker</a>
<a href="#struktura-plików">Struktura</a> <a href="#struktura-plików">Struktura</a>
<a href="#migawkisnapshoty">Snapshot</a><br> <a href="#migawkisnapshoty">Snapshot</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język</b> <b>Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język</b>
</p> </p>
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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

View File

@ -5,11 +5,11 @@
<a href="#como-compilar-com-docker">Docker</a> <a href="#como-compilar-com-docker">Docker</a>
<a href="#estrutura-de-arquivos">Estrutura</a> <a href="#estrutura-de-arquivos">Estrutura</a>
<a href="#screenshots">Screenshots</a><br> <a href="#screenshots">Screenshots</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Precisamos de sua ajuda para traduzir este README e a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI do RustDesk</a> para sua língua nativa</b> <b>Precisamos de sua ajuda para traduzir este README e a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI do RustDesk</a> para sua língua nativa</b>
</p> </p>
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) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)

View File

@ -1,11 +1,11 @@
<p align="center"> <p align="center">
<img src="logo-header.svg" alt="RustDesk - Your remote desktop"><br> <img src="logo-header.svg" alt="RustDesk - Ваш удаленый рабочий стол"><br>
<a href="#free-public-servers">Servers</a> <a href="#free-public-servers">Servers</a>
<a href="#raw-steps-to-build">Build</a> <a href="#raw-steps-to-build">Build</a>
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a> <a href="#file-structure">Structure</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Нам нужна ваша помощь для перевода этого README и <a href="https://github.com/rustdesk/rustdesk/tree/master/src/rustdesk/tree/master/src/lang">RustDesk UI</a> на ваш родной язык</B> <b>Нам нужна ваша помощь для перевода этого README и <a href="https://github.com/rustdesk/rustdesk/tree/master/src/rustdesk/tree/master/src/lang">RustDesk UI</a> на ваш родной язык</B>
</p> </p>
@ -15,10 +15,16 @@
Еще одно программное обеспечение для удаленного рабочего стола, написанное на Rust. Работает из коробки, не требует настройки. Вы полностью контролируете свои данные, не беспокоясь о безопасности. Вы можете использовать наш сервер ретрансляции, [настроить свой собственный](https://rustdesk.com/server), или [написать свой собственный сервер ретрансляции](https://github.com/rustdesk/rustdesk-server-demo). Еще одно программное обеспечение для удаленного рабочего стола, написанное на 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 приветствует вклад каждого. Смотрите [`CONTRIBUTING.md`](CONTRIBUTING.md) для помощи в начале работы.
[**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
[**СКАЧАТЬ ПРИЛОЖЕНИЕ**](https://github.com/rustdesk/rustdesk/releases) [**СКАЧАТЬ ПРИЛОЖЕНИЕ**](https://github.com/rustdesk/rustdesk/releases)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](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 vcpkg/vcpkg install libvpx libyuv opus
``` ```
### Исправление libvpx (Для Fedora) ### Исправление libvpx (для Fedora)
```sh ```sh
cd vcpkg/buildtrees/libvpx/src cd vcpkg/buildtrees/libvpx/src

182
README-VN.md Normal file
View File

@ -0,0 +1,182 @@
<p align="center">
<img src="logo-header.svg" alt="RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn"><br>
<a href="#free-public-servers">Máy chủ</a>
<a href="#raw-steps-to-build">Build</a>
<a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Cấu trúc tệp tin</a>
<a href="#snapshot">Snapshot</a><br>
[<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a><a href="https://github.com/rustdesk/doc.rustdesk.com">tài liệu</a> sang ngôn ngữ bản địa của bạn</b>
</p>
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)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](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 `<OPTIONAL-ARGS>`. 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)

View File

@ -5,7 +5,7 @@
<a href="#使用Docker编译">Docker</a> <a href="#使用Docker编译">Docker</a>
<a href="#文件结构">结构</a> <a href="#文件结构">结构</a>
<a href="#截图">截图</a><br> <a href="#截图">截图</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
</p> </p>
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) 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/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 截屏
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入 - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[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/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/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)**: 平台服务相关代码 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a> <a href="#file-structure">Structure</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>]<br> [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>]<br>
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> to your native language</b> <b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> to your native language</b>
</p> </p>
@ -155,7 +155,7 @@ Or, if you're running a release executable:
target/release/rustdesk 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 ## File Structure

42
build.py Normal file → Executable file
View File

@ -22,7 +22,7 @@ def get_version():
return '' return ''
def get_features(feature): def parse_rc_features(feature):
available_features = { available_features = {
'IddDriver': { 'IddDriver': {
'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64.zip', 'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64.zip',
@ -66,6 +66,11 @@ def make_parser():
default='', default='',
help='Integrate features, windows only.' help='Integrate features, windows only.'
'Available: IddDriver, PrivacyMode. Special value is "ALL" and empty "". Default is empty.') '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 return parser
@ -89,11 +94,9 @@ def download_extract_features(features, res_dir):
print(f'{feat} extract end') print(f'{feat} extract end')
def build_windows(feature): def get_rc_features(args):
features = get_features(feature) features = parse_rc_features(args.feature)
if not features: if features:
os.system('cargo build --release --features inline')
else:
print(f'Build with features {list(features.keys())}') print(f'Build with features {list(features.keys())}')
res_dir = 'resources' res_dir = 'resources'
if os.path.isdir(res_dir) and not os.path.islink(res_dir): if os.path.isdir(res_dir) and not os.path.islink(res_dir):
@ -102,9 +105,18 @@ def build_windows(feature):
raise Exception(f'Find file {res_dir}, not a directory') raise Exception(f'Find file {res_dir}, not a directory')
os.makedirs(res_dir, exist_ok=True) os.makedirs(res_dir, exist_ok=True)
download_extract_features(features, res_dir) 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(): def main():
parser = make_parser() parser = make_parser()
args = parser.parse_args() args = parser.parse_args()
@ -122,8 +134,9 @@ def main():
if os.path.isfile('/usr/bin/pacman'): if os.path.isfile('/usr/bin/pacman'):
os.system('git checkout src/ui/common.tis') os.system('git checkout src/ui/common.tis')
version = get_version() version = get_version()
features = ",".join(get_features(args))
if windows: if windows:
build_windows(args.feature) os.system('cargo build --release --features ' + features)
# os.system('upx.exe target/release/rustdesk.exe') # os.system('upx.exe target/release/rustdesk.exe')
os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe') os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe')
pa = os.environ.get('P') pa = os.environ.get('P')
@ -134,7 +147,7 @@ def main():
print('Not signed') print('Not signed')
os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe') os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe')
elif os.path.isfile('/usr/bin/pacman'): 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('git checkout src/ui/common.tis')
os.system('strip target/release/rustdesk') os.system('strip target/release/rustdesk')
os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) 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)) 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 # pacman -U ./rustdesk.pkg.tar.zst
elif os.path.isfile('/usr/bin/yum'): 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('strip target/release/rustdesk')
os.system("sed -i 's/Version: .*/Version: %s/g' rpm.spec" % version) os.system("sed -i 's/Version: .*/Version: %s/g' rpm.spec" % version)
os.system('HBB=`pwd` rpmbuild -ba rpm.spec') os.system('HBB=`pwd` rpmbuild -ba rpm.spec')
@ -151,14 +164,14 @@ def main():
version, version)) version, version))
# yum localinstall rustdesk.rpm # yum localinstall rustdesk.rpm
elif os.path.isfile('/usr/bin/zypper'): 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('strip target/release/rustdesk')
os.system("sed -i 's/Version: .*/Version: %s/g' rpm-suse.spec" % version) os.system("sed -i 's/Version: .*/Version: %s/g' rpm-suse.spec" % version)
os.system('HBB=`pwd` rpmbuild -ba rpm-suse.spec') 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)) os.system('mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % (version, version))
# yum localinstall rustdesk.rpm # yum localinstall rustdesk.rpm
else: else:
os.system('cargo bundle --release --features inline') os.system('cargo bundle --release --features ' + features)
if osx: if osx:
os.system( os.system(
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') '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('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
os.system( os.system(
'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') '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 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('strip tmpdeb/usr/bin/rustdesk')
os.system('mkdir -p tmpdeb/usr/lib/rustdesk') os.system('mkdir -p tmpdeb/usr/lib/rustdesk')
os.system('cp libsciter-gtk.so 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')
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user')
md5_file('usr/share/rustdesk/files/pynput_service.py') md5_file('usr/share/rustdesk/files/pynput_service.py')
md5_file('usr/lib/rustdesk/libsciter-gtk.so') md5_file('usr/lib/rustdesk/libsciter-gtk.so')
os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')

View File

@ -3,6 +3,7 @@
package="com.carriez.flutter_hbb"> package="com.carriez.flutter_hbb">
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@ -43,7 +44,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode|navigation"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:launchMode="singleTop" android:launchMode="singleTop"

View File

@ -8,14 +8,14 @@ package com.carriez.flutter_hbb
import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription import android.accessibilityservice.GestureDescription
import android.content.Context
import android.graphics.Path import android.graphics.Path
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import androidx.annotation.Keep
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import java.util.* import java.util.*
import kotlin.math.abs
import kotlin.math.max
const val LIFT_DOWN = 9 const val LIFT_DOWN = 9
const val LIFT_MOVE = 8 const val LIFT_MOVE = 8
@ -49,28 +49,40 @@ class InputService : AccessibilityService() {
private val wheelActionsQueue = LinkedList<GestureDescription>() private val wheelActionsQueue = LinkedList<GestureDescription>()
private var isWheelActionsPolling = false private var isWheelActionsPolling = false
private var isWaitingLongPress = false
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
fun onMouseInput(mask: Int, _x: Int, _y: Int) { fun onMouseInput(mask: Int, _x: Int, _y: Int) {
val x = if (_x < 0) { val x = max(0, _x)
0 val y = max(0, _y)
} else {
_x
}
val y = if (_y < 0) {
0
} else {
_y
}
if (mask == 0 || mask == LIFT_MOVE) { if (mask == 0 || mask == LIFT_MOVE) {
val oldX = mouseX
val oldY = mouseY
mouseX = x * SCREEN_INFO.scale mouseX = x * SCREEN_INFO.scale
mouseY = y * 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 // left button down ,was up
if (mask == LIFT_DOWN) { 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 leftIsDown = true
startGesture(mouseX, mouseY) startGesture(mouseX, mouseY)
return return
@ -83,9 +95,12 @@ class InputService : AccessibilityService() {
// left up ,was down // left up ,was down
if (mask == LIFT_UP) { if (mask == LIFT_UP) {
leftIsDown = false if (leftIsDown) {
endGesture(mouseX, mouseY) leftIsDown = false
return isWaitingLongPress = false
endGesture(mouseX, mouseY)
return
}
} }
if (mask == RIGHT_UP) { if (mask == RIGHT_UP) {

View File

@ -192,7 +192,6 @@ class MainActivity : FlutterActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
val inputPer = InputService.isOpen val inputPer = InputService.isOpen
Log.d(logTag, "onResume inputPer:$inputPer")
activity.runOnUiThread { activity.runOnUiThread {
flutterMethodChannel.invokeMethod( flutterMethodChannel.invokeMethod(
"on_state_changed", "on_state_changed",

View File

@ -2,20 +2,26 @@ package com.carriez.flutter_hbb
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent
import android.media.AudioRecord import android.media.AudioRecord
import android.media.AudioRecord.READ_BLOCKING import android.media.AudioRecord.READ_BLOCKING
import android.media.MediaCodecList import android.media.MediaCodecList
import android.media.MediaFormat import android.media.MediaFormat
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper 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.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService
import com.hjq.permissions.Permission import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions import com.hjq.permissions.XXPermissions
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.* import java.util.*
@SuppressLint("ConstantLocale") @SuppressLint("ConstantLocale")
val LOCAL_NAME = Locale.getDefault().toString() val LOCAL_NAME = Locale.getDefault().toString()
val SCREEN_INFO = Info(0, 0, 1, 200) val SCREEN_INFO = Info(0, 0, 1, 200)
@ -38,8 +44,31 @@ fun testVP9Support(): Boolean {
return res != null return res != null
} }
@RequiresApi(Build.VERSION_CODES.M)
fun requestPermission(context: Context, type: String) { fun requestPermission(context: Context, type: String) {
val permission = when (type) { 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" -> { "audio" -> {
Permission.RECORD_AUDIO Permission.RECORD_AUDIO
} }
@ -52,7 +81,7 @@ fun requestPermission(context: Context, type: String) {
} }
XXPermissions.with(context) XXPermissions.with(context)
.permission(permission) .permission(permission)
.request { permissions, all -> .request { _, all ->
if (all) { if (all) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod( 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 { fun checkPermission(context: Context, type: String): Boolean {
val permission = when (type) { val permission = when (type) {
"ignore_battery_optimizations" -> {
val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager
return pw.isIgnoringBatteryOptimizations(context.packageName)
}
"audio" -> { "audio" -> {
Permission.RECORD_AUDIO Permission.RECORD_AUDIO
} }

View File

@ -282,7 +282,12 @@ class PermissionManager {
static Timer? _timer; static Timer? _timer;
static var _current = ""; static var _current = "";
static final permissions = ["audio", "file"]; static final permissions = [
"audio",
"file",
"ignore_battery_optimizations",
"application_details_settings"
];
static bool isWaitingFile() { static bool isWaitingFile() {
if (_completer != null) { if (_completer != null) {
@ -301,6 +306,10 @@ class PermissionManager {
if (!permissions.contains(type)) if (!permissions.contains(type))
return Future.error("Wrong permission!$type"); return Future.error("Wrong permission!$type");
gFFI.invokeMethod("request_permission", type);
if (type == "ignore_battery_optimizations") {
return Future.value(false);
}
_current = type; _current = type;
_completer = Completer<bool>(); _completer = Completer<bool>();
gFFI.invokeMethod("request_permission", type); gFFI.invokeMethod("request_permission", type);
@ -328,6 +337,18 @@ class PermissionManager {
} }
} }
RadioListTile<T> getRadio<T>(
String name, T toValue, T curValue, void Function(T?) onChange) {
return RadioListTile<T>(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(translate(name)),
value: toValue,
groupValue: curValue,
onChanged: onChange,
dense: true,
);
}
/// find ffi, tag is Remote ID /// find ffi, tag is Remote ID
/// for session specific usage /// for session specific usage
FFI ffi(String? tag) { FFI ffi(String? tag) {

View File

@ -264,7 +264,6 @@ class _RemotePageState extends State<RemotePage> {
: SafeArea(child: : SafeArea(child:
OrientationBuilder(builder: (ctx, orientation) { OrientationBuilder(builder: (ctx, orientation) {
if (_currentOrientation != orientation) { if (_currentOrientation != orientation) {
debugPrint("on orientation changed");
Timer(Duration(milliseconds: 200), () { Timer(Duration(milliseconds: 200), () {
resetMobileActionsOverlay(); resetMobileActionsOverlay();
_currentOrientation = orientation; _currentOrientation = orientation;
@ -345,9 +344,14 @@ class _RemotePageState extends State<RemotePage> {
onKey: (data, e) { onKey: (data, e) {
final key = e.logicalKey; final key = e.logicalKey;
if (e is RawKeyDownEvent) { if (e is RawKeyDownEvent) {
if (e.repeat) { if (e.repeat &&
!e.isAltPressed &&
!e.isControlPressed &&
!e.isShiftPressed &&
!e.isMetaPressed) {
sendRawKey(e, press: true); sendRawKey(e, press: true);
} else { } else {
sendRawKey(e, down: true);
if (e.isAltPressed && !gFFI.alt) { if (e.isAltPressed && !gFFI.alt) {
gFFI.alt = true; gFFI.alt = true;
} else if (e.isControlPressed && !gFFI.ctrl) { } else if (e.isControlPressed && !gFFI.ctrl) {
@ -357,7 +361,6 @@ class _RemotePageState extends State<RemotePage> {
} else if (e.isMetaPressed && !gFFI.command) { } else if (e.isMetaPressed && !gFFI.command) {
gFFI.command = true; gFFI.command = true;
} }
sendRawKey(e, down: true);
} }
} }
// [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter // [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter
@ -483,6 +486,7 @@ class _RemotePageState extends State<RemotePage> {
/// DoubleFiner -> right click /// DoubleFiner -> right click
/// HoldDrag -> left drag /// HoldDrag -> left drag
Offset _cacheLongPressPosition = Offset(0, 0);
Widget getBodyForMobileWithGesture() { Widget getBodyForMobileWithGesture() {
final touchMode = gFFI.ffiModel.touchMode; final touchMode = gFFI.ffiModel.touchMode;
return getMixinGestureDetector( return getMixinGestureDetector(
@ -507,9 +511,14 @@ class _RemotePageState extends State<RemotePage> {
onLongPressDown: (d) { onLongPressDown: (d) {
if (touchMode) { if (touchMode) {
gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
_cacheLongPressPosition = d.localPosition;
} }
}, },
onLongPress: () { onLongPress: () {
if (touchMode) {
gFFI.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
}
gFFI.tap(MouseButtons.right); gFFI.tap(MouseButtons.right);
}, },
onDoubleFinerTap: (d) { onDoubleFinerTap: (d) {
@ -536,6 +545,15 @@ class _RemotePageState extends State<RemotePage> {
if (touchMode) { if (touchMode) {
gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
gFFI.sendMouse('down', MouseButtons.left); 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) { onOneFingerPanUpdate: (d) {
@ -946,18 +964,6 @@ CheckboxListTile getToggle(void Function(void Function()) setState, option, name
title: Text(translate(name))); title: Text(translate(name)));
} }
RadioListTile<String> getRadio(String name, String toValue, String curValue,
void Function(String?) onChange) {
return RadioListTile<String>(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(translate(name)),
value: toValue,
groupValue: curValue,
onChanged: onChange,
dense: true,
);
}
void showOptions() { void showOptions() {
String quality = gFFI.getByName('image_quality'); String quality = gFFI.getByName('image_quality');
if (quality == '') quality = 'balanced'; if (quality == '') quality = 'balanced';
@ -1045,6 +1051,8 @@ void showOptions() {
getRadio('Optimize reaction time', 'low', quality, setQuality), getRadio('Optimize reaction time', 'low', quality, setQuality),
Divider(color: MyTheme.border), Divider(color: MyTheme.border),
getToggle(setState, 'show-remote-cursor', 'Show remote cursor'), getToggle(setState, 'show-remote-cursor', 'Show remote cursor'),
getToggle(
setState, 'show-quality-monitor', 'Show quality monitor'),
] + ] +
more), more),
actions: [], actions: [],

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
@ -24,36 +26,84 @@ class ServerPage extends StatelessWidget implements PageShape {
return [ return [
PopupMenuItem( PopupMenuItem(
child: Text(translate("Change ID")), child: Text(translate("Change ID")),
padding: EdgeInsets.symmetric(horizontal: 16.0),
value: "changeID", value: "changeID",
enabled: false, enabled: false,
), ),
PopupMenuItem( PopupMenuItem(
child: Text(translate("Set your own password")), child: Text(translate("Set permanent password")),
value: "changePW", padding: EdgeInsets.symmetric(horizontal: 16.0),
enabled: gFFI.serverModel.isStart, value: "setPermanentPassword",
enabled:
gFFI.serverModel.verificationMethod != kUseTemporaryPassword,
), ),
PopupMenuItem( PopupMenuItem(
child: Text(translate("Refresh random password")), child: Text(translate("Set temporary password length")),
value: "refreshPW", padding: EdgeInsets.symmetric(horizontal: 16.0),
enabled: gFFI.serverModel.isStart, 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) { onSelected: (value) {
if (value == "changeID") { if (value == "changeID") {
// TODO // TODO
} else if (value == "changePW") { } else if (value == "setPermanentPassword") {
updatePasswordDialog(); setPermanentPasswordDialog();
} else if (value == "refreshPW") { } else if (value == "setTemporaryPasswordLength") {
() async { setTemporaryPasswordLengthDialog();
showLoading(translate("Waiting")); } else if (value == kUsePermanentPassword ||
if (await gFFI.serverModel.updatePassword("")) { value == kUseTemporaryPassword ||
showSuccess(); value == kUseBothPasswords) {
} else { Map<String, String> msg = Map()
showError(); ..["name"] = "verification-method"
} ..["value"] = value;
debugPrint("end updatePassword"); gFFI.setByName('option', jsonEncode(msg));
}(); gFFI.serverModel.updatePasswordModel();
} }
}) })
]; ];
@ -90,17 +140,13 @@ void checkService() async {
} }
} }
class ServerInfo extends StatefulWidget { class ServerInfo extends StatelessWidget {
@override
_ServerInfoState createState() => _ServerInfoState();
}
class _ServerInfoState extends State<ServerInfo> {
final model = gFFI.serverModel; final model = gFFI.serverModel;
var _passwdShow = false; final emptyController = TextEditingController(text: "-");
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isPermanent = model.verificationMethod == kUsePermanentPassword;
return model.isStart return model.isStart
? PaddingCard( ? PaddingCard(
child: Column( child: Column(
@ -123,24 +169,23 @@ class _ServerInfoState extends State<ServerInfo> {
), ),
TextFormField( TextFormField(
readOnly: true, readOnly: true,
obscureText: !_passwdShow,
style: TextStyle( style: TextStyle(
fontSize: 25.0, fontSize: 25.0,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: MyTheme.accent), color: MyTheme.accent),
controller: model.serverPasswd, controller: isPermanent ? emptyController : model.serverPasswd,
decoration: InputDecoration( decoration: InputDecoration(
icon: const Icon(Icons.lock), icon: const Icon(Icons.lock),
labelText: translate("Password"), labelText: translate("Password"),
labelStyle: TextStyle( labelStyle: TextStyle(
fontWeight: FontWeight.bold, color: MyTheme.accent50), fontWeight: FontWeight.bold, color: MyTheme.accent50),
suffix: IconButton( suffix: isPermanent
icon: Icon(Icons.visibility), ? null
onPressed: () { : IconButton(
setState(() { icon: const Icon(Icons.refresh),
_passwdShow = !_passwdShow; onPressed: () {
}); gFFI.setByName("temporary_password");
})), })),
onSaved: (String? value) {}, onSaved: (String? value) {},
), ),
], ],

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -26,13 +27,100 @@ class SettingsPage extends StatefulWidget implements PageShape {
_SettingsState createState() => _SettingsState(); _SettingsState createState() => _SettingsState();
} }
class _SettingsState extends State<SettingsPage> { class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
static const url = 'https://rustdesk.com/'; 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<bool> updateIgnoreBatteryStatus() async {
final res = await PermissionManager.check("ignore_battery_optimizations");
if (_ignoreBatteryOpt != res) {
setState(() {
_ignoreBatteryOpt = res;
});
return true;
} else {
return false;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Provider.of<FfiModel>(context); Provider.of<FfiModel>(context);
final username = getUsername(); 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<bool>(
(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( return SettingsList(
sections: [ sections: [
SettingsSection( SettingsSection(
@ -53,17 +141,17 @@ class _SettingsState extends State<SettingsPage> {
), ),
], ],
), ),
SettingsSection( SettingsSection(title: Text(translate("Settings")), tiles: [
title: Text(translate("Settings")), SettingsTile.navigation(
tiles: [
SettingsTile.navigation(
title: Text(translate('ID/Relay Server')), title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud), leading: Icon(Icons.cloud),
onPressed: (context) { onPressed: (context) {
showServerSettings(); showServerSettings();
}, })
), ]),
], SettingsSection(
title: Text(translate("Enhancements")),
tiles: enhancementsTiles,
), ),
SettingsSection( SettingsSection(
title: Text(translate("About")), title: Text(translate("About")),

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -21,9 +22,10 @@ void showError({Duration duration = SEC1}) {
showToast(translate("Error"), duration: SEC1); showToast(translate("Error"), duration: SEC1);
} }
void updatePasswordDialog() { void setPermanentPasswordDialog() {
final p0 = TextEditingController(); final pw = gFFI.getByName("permanent_password");
final p1 = TextEditingController(); final p0 = TextEditingController(text: pw);
final p1 = TextEditingController(text: pw);
var validateLength = false; var validateLength = false;
var validateSame = false; var validateSame = false;
DialogManager.show((setState, close) { DialogManager.show((setState, close) {
@ -87,7 +89,7 @@ void updatePasswordDialog() {
? () async { ? () async {
close(); close();
showLoading(translate("Waiting")); showLoading(translate("Waiting"));
if (await gFFI.serverModel.updatePassword(p0.text)) { if (await gFFI.serverModel.setPermanentPassword(p0.text)) {
showSuccess(); showSuccess();
} else { } else {
showError(); showError();
@ -101,6 +103,41 @@ void updatePasswordDialog() {
}); });
} }
void setTemporaryPasswordLengthDialog() {
List<String> 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<String, String> 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) { void enterPasswordDialog(String id) {
final controller = TextEditingController(); final controller = TextEditingController();
var remember = gFFI.getByName('remember', id) == 'true'; var remember = gFFI.getByName('remember', id) == 'true';

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
enum CustomTouchGestureState { enum GestureState {
none, none,
oneFingerPan, oneFingerPan,
twoFingerScale, twoFingerScale,
@ -35,64 +35,41 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate; GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate;
GestureDragEndCallback? onThreeFingerVerticalDragEnd; GestureDragEndCallback? onThreeFingerVerticalDragEnd;
var _currentState = CustomTouchGestureState.none; var _currentState = GestureState.none;
Timer? _startEventDebounceTimer; Timer? _debounceTimer;
void _init() { void _init() {
debugPrint("CustomTouchGestureRecognizer init"); debugPrint("CustomTouchGestureRecognizer init");
onStart = (d) { // onStart = (d) {};
_startEventDebounceTimer?.cancel(); onUpdate = (d) {
if (d.pointerCount == 1) { _debounceTimer?.cancel();
_currentState = CustomTouchGestureState.oneFingerPan; if (d.pointerCount == 1 && _currentState != GestureState.oneFingerPan) {
if (onOneFingerPanStart != null) { onOneFingerStartDebounce(d);
onOneFingerPanStart!(DragStartDetails( } else if (d.pointerCount == 2 &&
localPosition: d.localFocalPoint, globalPosition: d.focalPoint)); _currentState != GestureState.twoFingerScale) {
} onTwoFingerStartDebounce(d);
debugPrint("start oneFingerPan"); } else if (d.pointerCount == 3 &&
} else if (d.pointerCount == 2) { _currentState != GestureState.threeFingerVerticalDrag) {
if (_currentState == CustomTouchGestureState.threeFingerVerticalDrag) { _currentState = GestureState.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;
if (onThreeFingerVerticalDragStart != null) { if (onThreeFingerVerticalDragStart != null) {
onThreeFingerVerticalDragStart!( onThreeFingerVerticalDragStart!(
DragStartDetails(globalPosition: d.localFocalPoint)); DragStartDetails(globalPosition: d.localFocalPoint));
} }
debugPrint("start threeFingerScale"); debugPrint("start threeFingerScale");
// _reset();
} }
}; if (_currentState != GestureState.none) {
onUpdate = (d) {
if (_currentState != CustomTouchGestureState.none) {
switch (_currentState) { switch (_currentState) {
case CustomTouchGestureState.oneFingerPan: case GestureState.oneFingerPan:
if (onOneFingerPanUpdate != null) { if (onOneFingerPanUpdate != null) {
onOneFingerPanUpdate!(_getDragUpdateDetails(d)); onOneFingerPanUpdate!(_getDragUpdateDetails(d));
} }
break; break;
case CustomTouchGestureState.twoFingerScale: case GestureState.twoFingerScale:
if (onTwoFingerScaleUpdate != null) { if (onTwoFingerScaleUpdate != null) {
onTwoFingerScaleUpdate!(d); onTwoFingerScaleUpdate!(d);
} }
break; break;
case CustomTouchGestureState.threeFingerVerticalDrag: case GestureState.threeFingerVerticalDrag:
if (onThreeFingerVerticalDragUpdate != null) { if (onThreeFingerVerticalDragUpdate != null) {
onThreeFingerVerticalDragUpdate!(_getDragUpdateDetails(d)); onThreeFingerVerticalDragUpdate!(_getDragUpdateDetails(d));
} }
@ -105,21 +82,22 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
}; };
onEnd = (d) { onEnd = (d) {
debugPrint("ScaleGestureRecognizer onEnd"); debugPrint("ScaleGestureRecognizer onEnd");
_debounceTimer?.cancel();
// end // end
switch (_currentState) { switch (_currentState) {
case CustomTouchGestureState.oneFingerPan: case GestureState.oneFingerPan:
debugPrint("TwoFingerState.pan onEnd"); debugPrint("TwoFingerState.pan onEnd");
if (onOneFingerPanEnd != null) { if (onOneFingerPanEnd != null) {
onOneFingerPanEnd!(_getDragEndDetails(d)); onOneFingerPanEnd!(_getDragEndDetails(d));
} }
break; break;
case CustomTouchGestureState.twoFingerScale: case GestureState.twoFingerScale:
debugPrint("TwoFingerState.scale onEnd"); debugPrint("TwoFingerState.scale onEnd");
if (onTwoFingerScaleEnd != null) { if (onTwoFingerScaleEnd != null) {
onTwoFingerScaleEnd!(d); onTwoFingerScaleEnd!(d);
} }
break; break;
case CustomTouchGestureState.threeFingerVerticalDrag: case GestureState.threeFingerVerticalDrag:
debugPrint("ThreeFingerState.vertical onEnd"); debugPrint("ThreeFingerState.vertical onEnd");
if (onThreeFingerVerticalDragEnd != null) { if (onThreeFingerVerticalDragEnd != null) {
onThreeFingerVerticalDragEnd!(_getDragEndDetails(d)); onThreeFingerVerticalDragEnd!(_getDragEndDetails(d));
@ -128,10 +106,50 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
default: default:
break; 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 _getDragUpdateDetails(ScaleUpdateDetails d) =>
DragUpdateDetails( DragUpdateDetails(
globalPosition: d.focalPoint, globalPosition: d.focalPoint,

View File

@ -396,19 +396,19 @@ class ImageModel with ChangeNotifier {
} }
double get maxScale { double get maxScale {
if (_image == null) return 1.0; if (_image == null) return 1.5;
final size = MediaQueryData.fromWindow(ui.window).size; final size = MediaQueryData.fromWindow(ui.window).size;
final xscale = size.width / _image!.width; final xscale = size.width / _image!.width;
final yscale = size.height / _image!.height; final yscale = size.height / _image!.height;
return max(1.0, max(xscale, yscale)); return max(1.5, max(xscale, yscale));
} }
double get minScale { double get minScale {
if (_image == null) return 1.0; if (_image == null) return 1.5;
final size = MediaQueryData.fromWindow(ui.window).size; final size = MediaQueryData.fromWindow(ui.window).size;
final xscale = size.width / _image!.width; final xscale = size.width / _image!.width;
final yscale = size.height / _image!.height; final yscale = size.height / _image!.height;
return min(xscale, yscale); return min(xscale, yscale) / 1.5;
} }
} }

View File

@ -11,6 +11,10 @@ import 'model.dart';
const loginDialogTag = "LOGIN"; const loginDialogTag = "LOGIN";
const kUseTemporaryPassword = "use-temporary-password";
const kUsePermanentPassword = "use-permanent-password";
const kUseBothPasswords = "use-both-passwords";
class ServerModel with ChangeNotifier { class ServerModel with ChangeNotifier {
bool _isStart = false; // Android MainService status bool _isStart = false; // Android MainService status
bool _mediaOk = false; bool _mediaOk = false;
@ -18,6 +22,7 @@ class ServerModel with ChangeNotifier {
bool _audioOk = false; bool _audioOk = false;
bool _fileOk = false; bool _fileOk = false;
int _connectStatus = 0; // Rendezvous Server status int _connectStatus = 0; // Rendezvous Server status
String _verificationMethod = "";
late String _emptyIdShow; late String _emptyIdShow;
late final TextEditingController _serverId; late final TextEditingController _serverId;
@ -37,6 +42,8 @@ class ServerModel with ChangeNotifier {
int get connectStatus => _connectStatus; int get connectStatus => _connectStatus;
String get verificationMethod => _verificationMethod;
TextEditingController get serverId => _serverId; TextEditingController get serverId => _serverId;
TextEditingController get serverPasswd => _serverPasswd; TextEditingController get serverPasswd => _serverPasswd;
@ -112,9 +119,29 @@ class ServerModel with ChangeNotifier {
debugPrint("clients not match!"); debugPrint("clients not match!");
updateClientState(res); 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 { toggleAudio() async {
if (!_audioOk && !await PermissionManager.check("audio")) { if (!_audioOk && !await PermissionManager.check("audio")) {
final res = await PermissionManager.request("audio"); final res = await PermissionManager.request("audio");
@ -216,7 +243,7 @@ class ServerModel with ChangeNotifier {
parent.target?.ffiModel.updateEventListener(""); parent.target?.ffiModel.updateEventListener("");
await parent.target?.invokeMethod("init_service"); await parent.target?.invokeMethod("init_service");
parent.target?.setByName("start_service"); parent.target?.setByName("start_service");
getIDPasswd(); _fetchID();
updateClientState(); updateClientState();
if (!Platform.isLinux) { if (!Platform.isLinux) {
// current linux is not supported // current linux is not supported
@ -242,54 +269,33 @@ class ServerModel with ChangeNotifier {
await parent.target?.invokeMethod("init_input"); await parent.target?.invokeMethod("init_input");
} }
Future<bool> updatePassword(String pw) async { Future<bool> setPermanentPassword(String newPW) async {
final oldPasswd = _serverPasswd.text; parent.target?.setByName("permanent_password", newPW);
parent.target?.setByName("update_password", pw);
await Future.delayed(Duration(milliseconds: 500)); await Future.delayed(Duration(milliseconds: 500));
await getIDPasswd(force: true); final pw = parent.target?.getByName("permanent_password", newPW);
if (newPW == pw) {
// check result return true;
if (pw == "") {
if (_serverPasswd.text.isNotEmpty && _serverPasswd.text != oldPasswd) {
return true;
} else {
return false;
}
} else { } else {
if (_serverPasswd.text == pw) { return false;
return true;
} else {
return false;
}
} }
} }
getIDPasswd({bool force = false}) async { _fetchID() async {
if (!force && _serverId.text != _emptyIdShow && _serverPasswd.text != "") { final old = _serverId.text;
return;
}
var count = 0; var count = 0;
const maxCount = 10; const maxCount = 10;
while (count < maxCount) { while (count < maxCount) {
await Future.delayed(Duration(seconds: 1)); await Future.delayed(Duration(seconds: 1));
final id = parent.target?.getByName("server_id") ?? ""; final id = parent.target?.getByName("server_id") ?? "";
final passwd = parent.target?.getByName("server_password") ?? "";
if (id.isEmpty) { if (id.isEmpty) {
continue; continue;
} else { } else {
_serverId.text = id; _serverId.text = id;
} }
if (passwd.isEmpty) { debugPrint("fetch id again at $count:id:${_serverId.text}");
continue;
} else {
_serverPasswd.text = passwd;
}
debugPrint(
"fetch id & passwd again at $count:id:${_serverId.text},passwd:${_serverPasswd.text}");
count++; count++;
if (_serverId.text != _emptyIdShow && _serverPasswd.text.isNotEmpty) { if (_serverId.text != old) {
break; break;
} }
} }

60
lang.py
View File

@ -1,16 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Based on 'cn.rs', generate entries that are not completed in other languages
import os import os
import glob import glob
import sys
import csv
def get_lang(lang): def get_lang(lang):
out = {} out = {}
for ln in open('./src/lang/%s.rs'%lang): for ln in open('./src/lang/%s.rs'%lang):
ln = ln.strip() ln = ln.strip()
if ln.startswith('("'): if ln.startswith('("'):
k,v = line_split(ln) k, v = line_split(ln)
out[k] = v out[k] = v
return out return out
@ -19,28 +19,64 @@ def line_split(line):
assert(len(toks) == 2) assert(len(toks) == 2)
k = toks[0][2:] k = toks[0][2:]
v = toks[1][:-3] 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/*'): for fn in glob.glob('./src/lang/*'):
lang = os.path.basename(fn)[:-3] lang = os.path.basename(fn)[:-3]
if lang in ['en','cn']: continue if lang in ['en','cn']: continue
fw = open("%s.rs.gen"%lang, "wb+")
dict = get_lang(lang) dict = get_lang(lang)
fw = open("%s.rs"%lang, "wt")
for line in open('./src/lang/cn.rs'): for line in open('./src/lang/cn.rs'):
line_strip = line.strip() line_strip = line.strip()
if line_strip.startswith('("'): if line_strip.startswith('("'):
k,v = line_split(line_strip) k, v = line_split(line_strip)
if k in dict: if k in dict:
line = line.replace(v, dict[k]) line = line.replace(v, dict[k])
else: else:
line = line.replace(v, "") line = line.replace(v, "")
fw.write(line.encode()) fw.write(line)
else: else:
fw.write(line.encode()) fw.write(line)
fw.close() fw.close()
os.remove("./src/lang/%s.rs"%lang)
os.rename(fw.name, "./src/lang/%s.rs"%lang)
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() main()

View File

@ -22,6 +22,7 @@ appveyor = { repository = "pythoneer/enigo-85xiy" }
serde = { version = "1.0", optional = true } serde = { version = "1.0", optional = true }
serde_derive = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true }
log = "0.4" log = "0.4"
hbb_common = { path = "../hbb_common" }
[features] [features]
with_serde = ["serde", "serde_derive"] with_serde = ["serde", "serde_derive"]

View File

@ -249,7 +249,7 @@ pub trait MouseControllable {
/// For alphabetical keys, use Key::Layout for a system independent key. /// 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. /// If a key is missing, you can use the raw keycode with Key::Raw.
#[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))]
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Key { pub enum Key {
/// alt key on Linux and Windows (option key on macOS) /// alt key on Linux and Windows (option key on macOS)
Alt, Alt,

View File

@ -0,0 +1,5 @@
mod nix_impl;
mod pynput;
mod xdo;
pub use self::nix_impl::Enigo;

View File

@ -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<Box<dyn KeyboardControllable + Send>>,
uinput_mouse: Option<Box<dyn MouseControllable + Send>>,
}
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<Box<dyn KeyboardControllable + Send>>,
) {
self.uinput_keyboard = uinput_keyboard
}
/// Set uinput mouse.
pub fn set_uinput_mouse(&mut self, uinput_mouse: Option<Box<dyn MouseControllable + Send>>) {
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)
}
}
}
}

View File

@ -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;
}
});
}

View File

@ -3,7 +3,7 @@ use libc;
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
use self::libc::{c_char, c_int, c_void, useconds_t}; 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 CURRENT_WINDOW: c_int = 0;
const DEFAULT_DELAY: u64 = 12000; 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 /// The main struct for handling the event emitting
pub struct Enigo { pub(super) struct EnigoXdo {
xdo: Xdo, xdo: Xdo,
delay: u64, delay: u64,
tx: mpsc::Sender<(PyMsg, bool)>,
} }
// This is safe, we have a unique pointer. // This is safe, we have a unique pointer.
// TODO: use Unique<c_char> once stable. // TODO: use Unique<c_char> once stable.
unsafe impl Send for Enigo {} unsafe impl Send for EnigoXdo {}
impl Default for Enigo { impl Default for EnigoXdo {
/// Create a new Enigo instance /// Create a new EnigoXdo instance
fn default() -> Self { fn default() -> Self {
let (tx, rx) = mpsc::channel();
start_pynput_service(rx);
Self { Self {
xdo: unsafe { xdo_new(ptr::null()) }, xdo: unsafe { xdo_new(ptr::null()) },
delay: DEFAULT_DELAY, delay: DEFAULT_DELAY,
tx,
} }
} }
} }
impl Enigo { impl EnigoXdo {
/// Get the delay per keypress. /// Get the delay per keypress.
/// Default value is 12000. /// Default value is 12000.
/// This is Linux-specific. /// This is Linux-specific.
@ -99,101 +90,8 @@ impl Enigo {
pub fn set_delay(&mut self, delay: u64) { pub fn set_delay(&mut self, delay: u64) {
self.delay = delay; 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) { fn drop(&mut self) {
if self.xdo.is_null() { if self.xdo.is_null() {
return; 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) { fn mouse_move_to(&mut self, x: i32, y: i32) {
if self.xdo.is_null() { if self.xdo.is_null() {
return; 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 { fn get_key_state(&mut self, key: Key) -> bool {
if self.xdo.is_null() { if self.xdo.is_null() {
return false; return false;
@ -431,9 +329,6 @@ impl KeyboardControllable for Enigo {
if self.xdo.is_null() { if self.xdo.is_null() {
return Ok(()); return Ok(());
} }
if self.send_pynput(&key, true) {
return Ok(());
}
let string = CString::new(&*keysequence(key))?; let string = CString::new(&*keysequence(key))?;
unsafe { unsafe {
xdo_send_keysequence_window_down( xdo_send_keysequence_window_down(
@ -449,9 +344,6 @@ impl KeyboardControllable for Enigo {
if self.xdo.is_null() { if self.xdo.is_null() {
return; return;
} }
if self.send_pynput(&key, false) {
return;
}
if let Ok(string) = CString::new(&*keysequence(key)) { if let Ok(string) = CString::new(&*keysequence(key)) {
unsafe { unsafe {
xdo_send_keysequence_window_up( 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;
}
});
}

View File

@ -7,11 +7,11 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
protobuf = "3.0.0-alpha.2" protobuf = { version = "3.1", features = ["with-bytes"] }
tokio = { version = "1.15", features = ["full"] } tokio = { version = "1.20", features = ["full"] }
tokio-util = { version = "0.6", features = ["full"] } tokio-util = { version = "0.7", features = ["full"] }
futures = "0.3" futures = "0.3"
bytes = "1.1" bytes = { version = "1.2", features = ["serde"] }
log = "0.4" log = "0.4"
env_logger = "0.9" env_logger = "0.9"
socket2 = { version = "0.3", features = ["reuseport"] } socket2 = { version = "0.3", features = ["reuseport"] }
@ -23,6 +23,7 @@ directories-next = "2.0"
rand = "0.8" rand = "0.8"
serde_derive = "1.0" serde_derive = "1.0"
serde = "1.0" serde = "1.0"
serde_with = "1.14.0"
lazy_static = "1.4" lazy_static = "1.4"
confy = { git = "https://github.com/open-trade/confy" } confy = { git = "https://github.com/open-trade/confy" }
dirs-next = "2.0" 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] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1" mac_address = "1.1"
machine-uid = "0.2"
[features] [features]
quic = [] quic = []
[build-dependencies] [build-dependencies]
protobuf-codegen-pure = "3.0.0-alpha.2" protobuf-codegen = { version = "3.1" }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser"] } winapi = { version = "0.3", features = ["winuser"] }

View File

@ -1,9 +1,14 @@
fn main() { fn main() {
std::fs::create_dir_all("src/protos").unwrap(); std::fs::create_dir_all("src/protos").unwrap();
protobuf_codegen_pure::Codegen::new() protobuf_codegen::Codegen::new()
.pure()
.out_dir("src/protos") .out_dir("src/protos")
.inputs(&["protos/rendezvous.proto", "protos/message.proto"]) .inputs(&["protos/rendezvous.proto", "protos/message.proto"])
.include("protos") .include("protos")
.customize(
protobuf_codegen::Customize::default()
.tokio_bytes(true)
)
.run() .run()
.expect("Codegen failed."); .expect("Codegen failed.");
} }

View File

@ -1,13 +1,13 @@
syntax = "proto3"; syntax = "proto3";
package hbb; package hbb;
message VP9 { message EncodedVideoFrame {
bytes data = 1; bytes data = 1;
bool key = 2; bool key = 2;
int64 pts = 3; int64 pts = 3;
} }
message VP9s { repeated VP9 frames = 1; } message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; }
message RGB { bool compress = 1; } message RGB { bool compress = 1; }
@ -19,9 +19,11 @@ message YUV {
message VideoFrame { message VideoFrame {
oneof union { oneof union {
VP9s vp9s = 6; EncodedVideoFrames vp9s = 6;
RGB rgb = 7; RGB rgb = 7;
YUV yuv = 8; YUV yuv = 8;
EncodedVideoFrames h264s = 10;
EncodedVideoFrames h265s = 11;
} }
int64 timestamp = 9; int64 timestamp = 9;
} }
@ -61,6 +63,7 @@ message LoginRequest {
PortForward port_forward = 8; PortForward port_forward = 8;
} }
bool video_ack_required = 9; bool video_ack_required = 9;
uint64 session_id = 10;
} }
message ChatMessage { string text = 1; } message ChatMessage { string text = 1; }
@ -69,6 +72,11 @@ message Features {
bool privacy_mode = 1; bool privacy_mode = 1;
} }
message SupportedEncoding {
bool h264 = 1;
bool h265 = 2;
}
message PeerInfo { message PeerInfo {
string username = 1; string username = 1;
string hostname = 2; string hostname = 2;
@ -79,6 +87,7 @@ message PeerInfo {
string version = 7; string version = 7;
int32 conn_id = 8; int32 conn_id = 8;
Features features = 9; Features features = 9;
SupportedEncoding encoding = 10;
} }
message LoginResponse { message LoginResponse {
@ -417,6 +426,7 @@ message PermissionInfo {
Clipboard = 2; Clipboard = 2;
Audio = 3; Audio = 3;
File = 4; File = 4;
Restart = 5;
} }
Permission permission = 1; Permission permission = 1;
@ -430,6 +440,20 @@ enum ImageQuality {
Best = 4; 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 { message OptionMessage {
enum BoolOption { enum BoolOption {
NotSet = 0; NotSet = 0;
@ -445,11 +469,14 @@ message OptionMessage {
BoolOption disable_audio = 7; BoolOption disable_audio = 7;
BoolOption disable_clipboard = 8; BoolOption disable_clipboard = 8;
BoolOption enable_file_transfer = 9; BoolOption enable_file_transfer = 9;
VideoCodecState video_codec_state = 10;
} }
message TestDelay { message TestDelay {
int64 time = 1; int64 time = 1;
bool from_client = 2; bool from_client = 2;
uint32 last_delay = 3;
uint32 target_bitrate = 4;
} }
message PublicKey { message PublicKey {
@ -472,33 +499,33 @@ message AudioFrame {
message BackNotification { message BackNotification {
// no need to consider block input by someone else // no need to consider block input by someone else
enum BlockInputState { enum BlockInputState {
StateUnknown = 1; BlkStateUnknown = 0;
OnSucceeded = 2; BlkOnSucceeded = 2;
OnFailed = 3; BlkOnFailed = 3;
OffSucceeded = 4; BlkOffSucceeded = 4;
OffFailed = 5; BlkOffFailed = 5;
} }
enum PrivacyModeState { enum PrivacyModeState {
StateUnknown = 1; PrvStateUnknown = 0;
// Privacy mode on by someone else // Privacy mode on by someone else
OnByOther = 2; PrvOnByOther = 2;
// Privacy mode is not supported on the remote side // Privacy mode is not supported on the remote side
NotSupported = 3; PrvNotSupported = 3;
// Privacy mode on by self // Privacy mode on by self
OnSucceeded = 4; PrvOnSucceeded = 4;
// Privacy mode on by self, but denied // Privacy mode on by self, but denied
OnFailedDenied = 5; PrvOnFailedDenied = 5;
// Some plugins are not found // Some plugins are not found
OnFailedPlugin = 6; PrvOnFailedPlugin = 6;
// Privacy mode on by self, but failed // Privacy mode on by self, but failed
OnFailed = 7; PrvOnFailed = 7;
// Privacy mode off by self // Privacy mode off by self
OffSucceeded = 8; PrvOffSucceeded = 8;
// Ctrl + P // Ctrl + P
OffByPeer = 9; PrvOffByPeer = 9;
// Privacy mode off by self, but failed // Privacy mode off by self, but failed
OffFailed = 10; PrvOffFailed = 10;
OffUnknown = 11; PrvOffUnknown = 11;
} }
oneof union { oneof union {
@ -518,6 +545,7 @@ message Misc {
bool refresh_video = 10; bool refresh_video = 10;
bool video_received = 12; bool video_received = 12;
BackNotification back_notification = 13; BackNotification back_notification = 13;
bool restart_remote_device = 14;
} }
} }

View File

@ -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::{ use std::{
collections::HashMap, collections::HashMap,
fs, fs,
@ -12,11 +7,26 @@ use std::{
time::SystemTime, 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 RENDEZVOUS_TIMEOUT: u64 = 12_000;
pub const CONNECT_TIMEOUT: u64 = 18_000; pub const CONNECT_TIMEOUT: u64 = 18_000;
pub const REG_INTERVAL: i64 = 12_000; pub const REG_INTERVAL: i64 = 12_000;
pub const COMPRESS_LEVEL: i32 = 3; pub const COMPRESS_LEVEL: i32 = 3;
const SERIAL: i32 = 3; const SERIAL: i32 = 3;
const PASSWORD_ENC_VERSION: &'static str = "00";
// 128x128 // 128x128
#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding #[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= 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<Mutex<HashMap<String, i64>>> = Default::default(); pub static ref ONLINE: Arc<Mutex<HashMap<String, i64>>> = Default::default();
pub static ref PROD_RENDEZVOUS_SERVER: Arc<RwLock<String>> = Default::default(); pub static ref PROD_RENDEZVOUS_SERVER: Arc<RwLock<String>> = Default::default();
pub static ref APP_NAME: Arc<RwLock<String>> = Arc::new(RwLock::new("RustDesk".to_owned())); pub static ref APP_NAME: Arc<RwLock<String>> = Arc::new(RwLock::new("RustDesk".to_owned()));
static ref KEY_PAIR: Arc<Mutex<Option<(Vec<u8>, Vec<u8>)>>> = Default::default();
} }
// #[cfg(any(target_os = "android", target_os = "ios"))] // #[cfg(any(target_os = "android", target_os = "ios"))]
lazy_static::lazy_static! { lazy_static::lazy_static! {
pub static ref APP_DIR: Arc<RwLock<String>> = Default::default(); pub static ref APP_DIR: Arc<RwLock<String>> = Default::default();
@ -54,6 +66,7 @@ pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[
"rs-sg.rustdesk.com", "rs-sg.rustdesk.com",
"rs-cn.rustdesk.com", "rs-cn.rustdesk.com",
]; ];
pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=";
pub const RENDEZVOUS_PORT: i32 = 21116; pub const RENDEZVOUS_PORT: i32 = 21116;
pub const RELAY_PORT: i32 = 21117; pub const RELAY_PORT: i32 = 21117;
@ -72,7 +85,7 @@ pub struct Config {
#[serde(default)] #[serde(default)]
salt: String, salt: String,
#[serde(default)] #[serde(default)]
pub key_pair: (Vec<u8>, Vec<u8>), // sk, pk key_pair: (Vec<u8>, Vec<u8>), // sk, pk
#[serde(default)] #[serde(default)]
key_confirmed: bool, key_confirmed: bool,
#[serde(default)] #[serde(default)]
@ -107,7 +120,7 @@ pub struct Config2 {
pub options: HashMap<String, String>, pub options: HashMap<String, String>,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)] #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
pub struct PeerConfig { pub struct PeerConfig {
#[serde(default)] #[serde(default)]
pub password: Vec<u8>, pub password: Vec<u8>,
@ -139,6 +152,8 @@ pub struct PeerConfig {
pub disable_clipboard: bool, pub disable_clipboard: bool,
#[serde(default)] #[serde(default)]
pub enable_file_transfer: bool, pub enable_file_transfer: bool,
#[serde(default)]
pub show_quality_monitor: bool,
// the other scalar value must before this // the other scalar value must before this
#[serde(default)] #[serde(default)]
@ -159,7 +174,7 @@ pub struct PeerInfoSerde {
pub platform: String, pub platform: String,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)] #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
pub struct TransferSerde { pub struct TransferSerde {
#[serde(default)] #[serde(default)]
pub write_jobs: Vec<String>, pub write_jobs: Vec<String>,
@ -198,7 +213,17 @@ fn patch(path: PathBuf) -> PathBuf {
impl Config2 { impl Config2 {
fn load() -> Config2 { fn load() -> Config2 {
Config::load_::<Config2>("2") let mut config = Config::load_::<Config2>("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 { pub fn file() -> PathBuf {
@ -206,7 +231,12 @@ impl Config2 {
} }
fn store(&self) { 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 { pub fn get() -> Config2 {
@ -237,6 +267,11 @@ pub fn load_path<T: serde::Serialize + serde::de::DeserializeOwned + Default + s
cfg cfg
} }
#[inline]
pub fn store_path<T: serde::Serialize>(path: PathBuf, cfg: T) -> crate::ResultType<()> {
Ok(confy::store_path(path, cfg)?)
}
impl Config { impl Config {
fn load_<T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug>( fn load_<T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug>(
suffix: &str, suffix: &str,
@ -252,17 +287,25 @@ impl Config {
fn store_<T: serde::Serialize>(config: &T, suffix: &str) { fn store_<T: serde::Serialize>(config: &T, suffix: &str) {
let file = Self::file_(suffix); 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); log::error!("Failed to store config: {}", err);
} }
} }
fn load() -> Config { fn load() -> Config {
Config::load_::<Config>("") let mut config = Config::load_::<Config>("");
let (password, _, store) = decrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION);
config.password = password;
if store {
config.store();
}
config
} }
fn store(&self) { 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 { pub fn file() -> PathBuf {
@ -274,6 +317,10 @@ impl Config {
Config::with_extension(Self::path(name)) 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 { pub fn get_home() -> PathBuf {
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
return Self::path(APP_HOME_DIR.read().unwrap().as_str()); 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(); let mut rng = rand::thread_rng();
(0..6) (0..length)
.map(|_| CHARS[rng.gen::<usize>() % CHARS.len()]) .map(|_| CHARS[rng.gen::<usize>() % CHARS.len()])
.collect() .collect()
} }
@ -535,24 +582,26 @@ impl Config {
config.store(); config.store();
} }
pub fn set_key_pair(pair: (Vec<u8>, Vec<u8>)) {
let mut config = CONFIG.write().unwrap();
if config.key_pair == pair {
return;
}
config.key_pair = pair;
config.store();
}
pub fn get_key_pair() -> (Vec<u8>, Vec<u8>) { pub fn get_key_pair() -> (Vec<u8>, Vec<u8>) {
// lock here to make sure no gen_keypair more than once // 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_::<Config>("");
if config.key_pair.0.is_empty() { if config.key_pair.0.is_empty() {
let (pk, sk) = sign::gen_keypair(); let (pk, sk) = sign::gen_keypair();
config.key_pair = (sk.0.to_vec(), pk.0.into()); let key_pair = (sk.0.to_vec(), pk.0.into());
config.store(); 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 { pub fn get_id() -> String {
@ -618,7 +667,7 @@ impl Config {
log::info!("id updated from {} to {}", id, new_id); 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(); let mut config = CONFIG.write().unwrap();
if password == config.password { if password == config.password {
return; return;
@ -627,13 +676,8 @@ impl Config {
config.store(); config.store();
} }
pub fn get_password() -> String { pub fn get_permanent_password() -> String {
let mut password = CONFIG.read().unwrap().password.clone(); CONFIG.read().unwrap().password.clone()
if password.is_empty() {
password = Config::get_auto_password();
Config::set_password(&password);
}
password
} }
pub fn set_salt(salt: &str) { pub fn set_salt(salt: &str) {
@ -648,7 +692,7 @@ impl Config {
pub fn get_salt() -> String { pub fn get_salt() -> String {
let mut salt = CONFIG.read().unwrap().salt.clone(); let mut salt = CONFIG.read().unwrap().salt.clone();
if salt.is_empty() { if salt.is_empty() {
salt = Config::get_auto_password(); salt = Config::get_auto_password(6);
Config::set_salt(&salt); Config::set_salt(&salt);
} }
salt salt
@ -705,7 +749,28 @@ impl PeerConfig {
pub fn load(id: &str) -> PeerConfig { pub fn load(id: &str) -> PeerConfig {
let _ = CONFIG.read().unwrap(); // for lock let _ = CONFIG.read().unwrap(); // for lock
match confy::load_path(&Self::path(id)) { 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) => { Err(err) => {
log::error!("Failed to load config: {}", err); log::error!("Failed to load config: {}", err);
Default::default() Default::default()
@ -715,7 +780,17 @@ impl PeerConfig {
pub fn store(&self, id: &str) { pub fn store(&self, id: &str) {
let _ = CONFIG.read().unwrap(); // for lock 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); 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<String, String>,
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)] #[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct LanPeers { pub struct LanPeers {
#[serde(default)] pub peers: Vec<DiscoveryPeer>,
pub peers: String,
} }
impl LanPeers { impl LanPeers {
@ -865,9 +956,11 @@ impl LanPeers {
} }
} }
pub fn store(peers: String) { pub fn store(peers: &Vec<DiscoveryPeer>) {
let f = LanPeers { peers }; let f = LanPeers {
if let Err(err) = confy::store_path(Config::file_("_lan_peers"), f) { peers: peers.clone(),
};
if let Err(err) = store_path(Config::file_("_lan_peers"), f) {
log::error!("Failed to store lan peers: {}", err); 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<String, String>,
}
impl HwCodecConfig {
pub fn load() -> HwCodecConfig {
Config::load_::<HwCodecConfig>("_hwcodec")
}
pub fn store(&self) {
Config::store_(self, "_hwcodec");
}
pub fn remove() {
std::fs::remove_file(Config::file_("_hwcodec")).ok();
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -573,7 +573,7 @@ impl TransferJob {
log::info!("file num truncated, ignoring"); log::info!("file num truncated, ignoring");
} else { } else {
match r.union { match r.union {
Some(file_transfer_send_confirm_request::Union::skip(s)) => { Some(file_transfer_send_confirm_request::Union::Skip(s)) => {
if s { if s {
log::debug!("skip file id:{}, file_num:{}", r.id, r.file_num); log::debug!("skip file id:{}, file_num:{}", r.id, r.file_num);
self.skip_current_file(); self.skip_current_file();
@ -581,7 +581,7 @@ impl TransferJob {
self.set_file_confirmed(true); 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); self.set_file_confirmed(true);
} }
_ => {} _ => {}

View File

@ -1,11 +1,12 @@
pub mod compress; pub mod compress;
#[path = "./protos/message.rs"] pub mod platform;
pub mod message_proto; pub mod protos;
#[path = "./protos/rendezvous.rs"]
pub mod rendezvous_proto;
pub use bytes; pub use bytes;
use config::Config;
pub use futures; pub use futures;
pub use protobuf; pub use protobuf;
pub use protos::message as message_proto;
pub use protos::rendezvous as rendezvous_proto;
use std::{ use std::{
fs::File, fs::File,
io::{self, BufRead}, io::{self, BufRead},
@ -27,6 +28,7 @@ pub use anyhow::{self, bail};
pub use futures_util; pub use futures_util;
pub mod config; pub mod config;
pub mod fs; pub mod fs;
pub use lazy_static;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use mac_address; pub use mac_address;
pub use rand; pub use rand;
@ -35,7 +37,7 @@ pub use sodiumoxide;
pub use tokio_socks; pub use tokio_socks;
pub use tokio_socks::IntoTargetAddr; pub use tokio_socks::IntoTargetAddr;
pub use tokio_socks::TargetAddr; pub use tokio_socks::TargetAddr;
pub use lazy_static; pub mod password_security;
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
pub type Stream = quic::Connection; pub type Stream = quic::Connection;
@ -200,6 +202,14 @@ pub fn get_modified_time(path: &std::path::Path) -> SystemTime {
.unwrap_or(UNIX_EPOCH) .unwrap_or(UNIX_EPOCH)
} }
pub fn get_uuid() -> Vec<u8> {
#[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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -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<RwLock<String>> = 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<u8> {
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<u8>, 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<String, ()> {
if v.len() > 0 {
symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
} else {
Err(())
}
}
fn decrypt(v: &[u8]) -> Result<Vec<u8>, ()> {
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<Vec<u8>, ()> {
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<u8> = 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<u8> = 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);
}
}

View File

@ -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<String> {
let output = std::process::Command::new("sh")
.args(vec!["-c", &cmds])
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}

View File

@ -0,0 +1,2 @@
#[cfg(target_os = "linux")]
pub mod linux;

View File

@ -27,6 +27,8 @@ fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result<Socket,
socket.set_reuse_port(true)?; socket.set_reuse_port(true)?;
socket.set_reuse_address(true)?; socket.set_reuse_address(true)?;
} }
// only nonblocking work with tokio, https://stackoverflow.com/questions/64649405/receiver-on-tokiompscchannel-only-receives-messages-when-buffer-is-full
socket.set_nonblocking(true)?;
if buf_size > 0 { if buf_size > 0 {
socket.set_recv_buffer_size(buf_size).ok(); socket.set_recv_buffer_size(buf_size).ok();
} }

View File

@ -18,6 +18,7 @@ cfg-if = "1.0"
libc = "0.2" libc = "0.2"
num_cpus = "1.13" num_cpus = "1.13"
lazy_static = "1.4" lazy_static = "1.4"
hbb_common = { path = "../hbb_common" }
[dependencies.winapi] [dependencies.winapi]
version = "0.3" version = "0.3"
@ -48,3 +49,6 @@ tracing = { version = "0.1", optional = true }
gstreamer = { version = "0.16", optional = true } gstreamer = { version = "0.16", optional = true }
gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
gstreamer-video = { version = "0.16", 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 }

View File

@ -4,7 +4,7 @@ extern crate scrap;
use std::fs::File; use std::fs::File;
#[cfg(windows)] #[cfg(windows)]
use scrap::CapturerMag; use scrap::{CapturerMag, TraitCapturer};
use scrap::{i420_to_rgb, Display}; use scrap::{i420_to_rgb, Display};
fn main() { fn main() {
@ -21,6 +21,8 @@ fn get_display(i: usize) -> Display {
#[cfg(windows)] #[cfg(windows)]
fn record(i: usize) { fn record(i: usize) {
use std::time::Duration;
for d in Display::all().unwrap() { for d in Display::all().unwrap() {
println!("{:?} {} {}", d.origin(), d.width(), d.height()); 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); 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()); println!("Capture data len: {}, Saving...", frame.len());
let mut bitflipped = Vec::with_capacity(w * h * 4); 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); 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()); println!("Capture data len: {}, Saving...", buffer.len());
let mut frame = Default::default(); let mut frame = Default::default();

View File

@ -1,7 +1,9 @@
use std::time::Duration;
extern crate scrap; extern crate scrap;
fn main() { fn main() {
use scrap::{Capturer, Display}; use scrap::{Capturer, Display, TraitCapturer};
use std::io::ErrorKind::WouldBlock; use std::io::ErrorKind::WouldBlock;
use std::io::Write; use std::io::Write;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
@ -29,7 +31,7 @@ fn main() {
let mut out = child.stdin.unwrap(); let mut out = child.stdin.unwrap();
loop { loop {
match capturer.frame(0) { match capturer.frame(Duration::from_millis(0)) {
Ok(frame) => { Ok(frame) => {
// Write the frame, removing end-of-row padding. // Write the frame, removing end-of-row padding.
let stride = frame.len() / h; let stride = frame.len() / h;

View File

@ -13,11 +13,12 @@ use std::time::{Duration, Instant};
use std::{io, thread}; use std::{io, thread};
use docopt::Docopt; use docopt::Docopt;
use scrap::codec::{EncoderApi, EncoderCfg};
use webm::mux; use webm::mux;
use webm::mux::Track; use webm::mux::Track;
use scrap::codec as vpx_encode; use scrap::vpxcodec as vpx_encode;
use scrap::{Capturer, Display, STRIDE_ALIGN}; use scrap::{TraitCapturer, Capturer, Display, STRIDE_ALIGN};
const USAGE: &'static str = " const USAGE: &'static str = "
Simple WebM screen capture. 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."); mux::Segment::new(mux::Writer::new(out)).expect("Could not initialize the multiplexer.");
let (vpx_codec, mux_codec) = match args.flag_codec { let (vpx_codec, mux_codec) = match args.flag_codec {
Codec::Vp8 => (vpx_encode::VideoCodecId::VP8, mux::VideoCodecId::VP8), Codec::Vp8 => (vpx_encode::VpxVideoCodecId::VP8, mux::VideoCodecId::VP8),
Codec::Vp9 => (vpx_encode::VideoCodecId::VP9, mux::VideoCodecId::VP9), Codec::Vp9 => (vpx_encode::VpxVideoCodecId::VP9, mux::VideoCodecId::VP9),
}; };
let mut vt = webm.add_video_track(width, height, None, mux_codec); let mut vt = webm.add_video_track(width, height, None, mux_codec);
// Setup the encoder. // Setup the encoder.
let mut vpx = vpx_encode::Encoder::new( let mut vpx = vpx_encode::VpxEncoder::new(EncoderCfg::VPX(vpx_encode::VpxEncoderConfig {
&vpx_encode::Config { width,
width, height,
height, timebase: [1, 1000],
timebase: [1, 1000], bitrate: args.flag_bv,
bitrate: args.flag_bv, codec: vpx_codec,
codec: vpx_codec, num_threads: 0,
rc_min_quantizer: 0, }))
rc_max_quantizer: 0,
speed: 6,
},
0,
)
.unwrap(); .unwrap();
// Start recording. // Start recording.
@ -138,7 +134,7 @@ fn main() -> io::Result<()> {
break; 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; let ms = time.as_secs() * 1000 + time.subsec_millis() as u64;
for frame in vpx.encode(ms as i64, &frame, STRIDE_ALIGN).unwrap() { for frame in vpx.encode(ms as i64, &frame, STRIDE_ALIGN).unwrap() {

View File

@ -6,7 +6,7 @@ use std::io::ErrorKind::WouldBlock;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use scrap::{i420_to_rgb, Capturer, Display}; use scrap::{i420_to_rgb, Capturer, Display, TraitCapturer};
fn main() { fn main() {
let n = Display::all().unwrap().len(); let n = Display::all().unwrap().len();
@ -34,7 +34,7 @@ fn record(i: usize) {
loop { loop {
// Wait until there's a frame. // Wait until there's a frame.
let buffer = match capturer.frame(0) { let buffer = match capturer.frame(Duration::from_millis(0)) {
Ok(buffer) => buffer, Ok(buffer) => buffer,
Err(error) => { Err(error) => {
if error.kind() == WouldBlock { if error.kind() == WouldBlock {
@ -83,7 +83,7 @@ fn record(i: usize) {
loop { loop {
// Wait until there's a frame. // Wait until there's a frame.
let buffer = match capturer.frame(0) { let buffer = match capturer.frame(Duration::from_millis(0)) {
Ok(buffer) => buffer, Ok(buffer) => buffer,
Err(error) => { Err(error) => {
if error.kind() == WouldBlock { if error.kind() == WouldBlock {

View File

@ -3,8 +3,8 @@ use crate::rgba_to_i420;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
use std::io;
use std::sync::Mutex; use std::sync::Mutex;
use std::{io, time::Duration};
lazy_static! { lazy_static! {
static ref SCREEN_SIZE: Mutex<(u16, u16, u16)> = Mutex::new((0, 0, 0)); // (width, height, scale) 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 { pub fn height(&self) -> usize {
self.display.height() as usize self.display.height() as usize
} }
}
pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result<Frame<'a>> { impl crate::TraitCapturer for Capturer {
fn set_use_yuv(&mut self, _use_yuv: bool) {}
fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result<Frame<'a>> {
if let Some(buf) = get_video_raw() { if let Some(buf) = get_video_raw() {
crate::would_block_if_equal(&mut self.saved_raw_data, buf)?; crate::would_block_if_equal(&mut self.saved_raw_data, buf)?;
rgba_to_i420(self.width(), self.height(), buf, &mut self.bgra); rgba_to_i420(self.width(), self.height(), buf, &mut self.bgra);

View File

@ -1,536 +1,372 @@
// https://github.com/astraw/vpx-encode use std::ops::{Deref, DerefMut};
// https://github.com/astraw/env-libvpx-sys #[cfg(feature = "hwcodec")]
// https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *}; #[cfg(feature = "hwcodec")]
use std::os::raw::{c_int, c_uint}; use crate::hwcodec::*;
use std::{ptr, slice}; use crate::vpxcodec::*;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] use hbb_common::{
pub enum VideoCodecId { anyhow::anyhow,
VP8, log,
VP9, 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<Mutex<HashMap<i32, VideoCodecState>>> = 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,
} }
impl Default for VideoCodecId { #[derive(Debug, Clone)]
fn default() -> VideoCodecId { pub enum EncoderCfg {
VideoCodecId::VP9 VPX(VpxEncoderConfig),
} HW(HwEncoderConfig),
}
pub trait EncoderApi {
fn new(cfg: EncoderCfg) -> ResultType<Self>
where
Self: Sized;
fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType<Message>;
fn use_yuv(&self) -> bool;
fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()>;
}
pub struct DecoderCfg {
pub vpx: VpxDecoderConfig,
} }
pub struct Encoder { pub struct Encoder {
ctx: vpx_codec_ctx_t, pub codec: Box<dyn EncoderApi>,
width: usize, }
height: usize,
impl Deref for Encoder {
type Target = Box<dyn EncoderApi>;
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 { pub struct Decoder {
ctx: vpx_codec_ctx_t, vpx: VpxDecoder,
#[cfg(feature = "hwcodec")]
hw: HwDecoders,
#[cfg(feature = "hwcodec")]
i420: Vec<u8>,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum Error { pub enum EncoderUpdate {
FailedCall(String), State(VideoCodecState),
BadPtr(String), Remove,
} DisableHwIfNotExist,
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<T> = std::result::Result<T, Error>;
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 { impl Encoder {
pub fn new(config: &Config, num_threads: u32) -> Result<Self> { pub fn new(config: EncoderCfg) -> ResultType<Encoder> {
let i; log::info!("new encoder:{:?}", config);
if cfg!(feature = "VP8") { match config {
i = match config.codec { EncoderCfg::VPX(_) => Ok(Encoder {
VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()), codec: Box::new(VpxEncoder::new(config)?),
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/ #[cfg(feature = "hwcodec")]
// default: c.rc_min_quantizer = 0, c.rc_max_quantizer = 63 EncoderCfg::HW(_) => match HwEncoder::new(config) {
// try rc_resize_allowed later Ok(hw) => Ok(Encoder {
codec: Box::new(hw),
c.g_w = config.width; }),
c.g_h = config.height; Err(e) => {
c.g_timebase.num = config.timebase[0]; check_config_process(true);
c.g_timebase.den = config.timebase[1]; Err(e)
c.rc_target_bitrate = config.bitrate; }
c.rc_undershoot_pct = 95; },
c.rc_dropframe_thresh = 25; #[cfg(not(feature = "hwcodec"))]
if config.rc_min_quantizer > 0 { _ => Err(anyhow!("unsupported encoder type")),
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 encoderate 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<EncodeFrames> {
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<EncodeFrames> {
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)] // TODO
pub struct EncodeFrame<'a> { pub fn update_video_encoder(id: i32, update: EncoderUpdate) {
/// Compressed data. #[cfg(feature = "hwcodec")]
pub data: &'a [u8], {
/// Whether the frame is a keyframe. let mut states = PEER_DECODER_STATES.lock().unwrap();
pub key: bool, match update {
/// Presentation timestamp (in timebase units). EncoderUpdate::State(state) => {
pub pts: i64, states.insert(id, state);
} }
EncoderUpdate::Remove => {
#[derive(Clone, Copy, Debug)] states.remove(&id);
pub struct Config { }
/// The width (in pixels). EncoderUpdate::DisableHwIfNotExist => {
pub width: c_uint, if !states.contains_key(&id) {
/// The height (in pixels). states.insert(id, VideoCodecState::default());
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<Self::Item> {
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.
} }
} }
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::<i32>();
if enabled_h264 {
score_h264 += states.iter().map(|s| s.1.score_h264).sum::<i32>();
}
if enabled_h265 {
score_h265 += states.iter().map(|s| s.1.score_h265).sum::<i32>();
}
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<String> {
#[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 { impl Decoder {
/// Create a new decoder pub fn video_codec_state(_id: &str) -> VideoCodecState {
/// #[cfg(feature = "hwcodec")]
/// # Errors if check_hwcodec_config() {
/// let best = HwDecoder::best();
/// The function may fail if the underlying libvpx does not provide VideoCodecState {
/// the VP9 decoder. score_vpx: SCORE_VPX,
pub fn new(codec: VideoCodecId, num_threads: u32) -> Result<Self> { score_h264: best.h264.map_or(0, |c| c.score),
// This is sound because `vpx_codec_ctx` is a repr(C) struct without any field that can score_h265: best.h265.map_or(0, |c| c.score),
// cause UB if uninitialized. perfer: Self::codec_preference(_id).into(),
let i; ..Default::default()
if cfg!(feature = "VP8") { }
i = match codec { } else {
VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()), return VideoCodecState {
VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()), score_vpx: SCORE_VPX,
..Default::default()
}; };
} else {
i = call_vpx_ptr!(vpx_codec_vp9_dx());
} }
let mut ctx = Default::default(); #[cfg(not(feature = "hwcodec"))]
let cfg = vpx_codec_dec_cfg_t { VideoCodecState {
threads: if num_threads == 0 { score_vpx: SCORE_VPX,
num_cpus::get() as _ ..Default::default()
} 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<Vec<u8>> {
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 pub fn new(config: DecoderCfg) -> Decoder {
/// let vpx = VpxDecoder::new(config.vpx).unwrap();
/// The `data` slice is sent to the decoder Decoder {
/// vpx,
/// It matches a call to `vpx_codec_decode`. #[cfg(feature = "hwcodec")]
pub fn decode(&mut self, data: &[u8]) -> Result<DecodeFrames> { hw: HwDecoder::new_decoders(),
call_vpx!(vpx_codec_decode( #[cfg(feature = "hwcodec")]
&mut self.ctx, i420: vec![],
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 handle_video_frame(
pub fn flush(&mut self) -> Result<DecodeFrames> { &mut self,
call_vpx!(vpx_codec_decode( frame: &video_frame::Union,
&mut self.ctx, rgb: &mut Vec<u8>,
ptr::null(), ) -> ResultType<bool> {
0, match frame {
ptr::null_mut(), video_frame::Union::Vp9s(vp9s) => {
0 Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, rgb)
)); }
Ok(DecodeFrames { #[cfg(feature = "hwcodec")]
ctx: &mut self.ctx, video_frame::Union::H264s(h264s) => {
iter: ptr::null(), 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!")),
}
} }
}
impl Drop for Decoder { fn handle_vp9s_video_frame(
fn drop(&mut self) { decoder: &mut VpxDecoder,
unsafe { vp9s: &EncodedVideoFrames,
let result = vpx_codec_destroy(&mut self.ctx); rgb: &mut Vec<u8>,
if result != VPX_CODEC_OK { ) -> ResultType<bool> {
panic!("failed to destroy vpx codec"); 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;
pub struct DecodeFrames<'a> { }
ctx: &'a mut vpx_codec_ctx_t, if last_frame.is_null() {
iter: vpx_codec_iter_t, Ok(false)
}
impl<'a> Iterator for DecodeFrames<'a> {
type Item = Image;
fn next(&mut self) -> Option<Self::Item> {
let img = unsafe { vpx_codec_get_frame(self.ctx, &mut self.iter) };
if img.is_null() {
return None;
} else { } else {
return Some(Image(img)); last_frame.rgb(1, true, rgb);
Ok(true)
} }
} }
}
// https://chromium.googlesource.com/webm/libvpx/+/bali/vpx/src/vpx_image.c #[cfg(feature = "hwcodec")]
pub struct Image(*mut vpx_image_t); fn handle_hw_video_frame(
impl Image { decoder: &mut HwDecoder,
#[inline] frames: &EncodedVideoFrames,
pub fn new() -> Self { rgb: &mut Vec<u8>,
Self(std::ptr::null_mut()) i420: &mut Vec<u8>,
} ) -> ResultType<bool> {
let mut ret = false;
#[inline] for h264 in frames.frames.iter() {
pub fn is_null(&self) -> bool { for image in decoder.decode(&h264.data)? {
self.0.is_null() // TODO: just process the last frame
} if image.bgra(rgb, i420).is_ok() {
ret = true;
#[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<u8>) {
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 _,
);
} }
} }
return Ok(ret);
} }
#[inline] #[cfg(feature = "hwcodec")]
pub fn data(&self) -> (&[u8], &[u8], &[u8]) { fn codec_preference(id: &str) -> PerferCodec {
unsafe { let codec = PeerConfig::load(id)
let img = self.inner(); .options
let h = (img.d_h as usize + 1) & !1; .get("codec-preference")
let n = img.stride[0] as usize * h; .map_or("".to_owned(), |c| c.to_owned());
let y = slice::from_raw_parts(img.planes[0], n); if codec == "vp9" {
let n = img.stride[1] as usize * (h >> 1); PerferCodec::VPX
let u = slice::from_raw_parts(img.planes[1], n); } else if codec == "h264" {
let v = slice::from_raw_parts(img.planes[2], n); PerferCodec::H264
(y, u, v) } else if codec == "h265" {
PerferCodec::H265
} else {
PerferCodec::Auto
} }
} }
} }
impl Drop for Image { #[cfg(feature = "hwcodec")]
fn drop(&mut self) { fn check_hwcodec_config() -> bool {
if !self.0.is_null() { if let Some(v) = Config2::get().options.get("enable-hwcodec") {
unsafe { vpx_img_free(self.0) }; return v != "N";
}
} }
return true; // default is true
} }
unsafe impl Send for vpx_codec_ctx_t {}

View File

@ -49,6 +49,17 @@ extern "C" {
height: c_int, height: c_int,
) -> 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( pub fn NV12ToI420(
src_y: *const u8, src_y: *const u8,
src_stride_y: c_int, src_stride_y: c_int,
@ -91,6 +102,17 @@ extern "C" {
width: c_int, width: c_int,
height: c_int, height: c_int,
) -> 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 // https://github.com/webmproject/libvpx/blob/master/vpx/src/vpx_image.c
@ -220,3 +242,195 @@ pub unsafe fn nv12_to_i420(
height as _, 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<u8>,
) {
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<u8>,
) {
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<u8>,
i420: &mut Vec<u8>,
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<u8>,
_i420: &mut Vec<u8>,
_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<u8>,
) {
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 _,
);
};
}
}

View File

@ -1,6 +1,12 @@
use crate::dxgi; use crate::{common::TraitCapturer, dxgi};
use std::io::ErrorKind::{NotFound, TimedOut, WouldBlock}; use std::{
use std::{io, ops}; io::{
self,
ErrorKind::{NotFound, TimedOut, WouldBlock},
},
ops,
time::Duration,
};
pub struct Capturer { pub struct Capturer {
inner: dxgi::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) { pub fn cancel_gdi(&mut self) {
self.inner.cancel_gdi() self.inner.cancel_gdi()
} }
@ -39,14 +37,28 @@ impl Capturer {
pub fn height(&self) -> usize { pub fn height(&self) -> usize {
self.height self.height
} }
}
pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result<Frame<'a>> { impl TraitCapturer for Capturer {
match self.inner.frame(timeout_ms) { 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<Frame<'a>> {
match self.inner.frame(timeout.as_millis() as _) {
Ok(frame) => Ok(Frame(frame)), Ok(frame) => Ok(Frame(frame)),
Err(ref error) if error.kind() == TimedOut => Err(WouldBlock.into()), Err(ref error) if error.kind() == TimedOut => Err(WouldBlock.into()),
Err(error) => Err(error), 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]); pub struct Frame<'a>(&'a [u8]);
@ -128,6 +140,7 @@ impl CapturerMag {
data: Vec::new(), data: Vec::new(),
}) })
} }
pub fn exclude(&mut self, cls: &str, name: &str) -> io::Result<bool> { pub fn exclude(&mut self, cls: &str, name: &str) -> io::Result<bool> {
self.inner.exclude(cls, name) self.inner.exclude(cls, name)
} }
@ -135,8 +148,23 @@ impl CapturerMag {
pub fn get_rect(&self) -> ((i32, i32), usize, usize) { pub fn get_rect(&self) -> ((i32, i32), usize, usize) {
self.inner.get_rect() self.inner.get_rect()
} }
pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result<Frame<'a>> { }
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<Frame<'a>> {
self.inner.frame(&mut self.data)?; self.inner.frame(&mut self.data)?;
Ok(Frame(&self.data)) Ok(Frame(&self.data))
} }
fn is_gdi(&self) -> bool {
false
}
fn set_gdi(&mut self) -> bool {
false
}
} }

View File

@ -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<Mutex<Option<String>>> = 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<u8>,
pub format: DataFormat,
pub pixfmt: AVPixelFormat,
}
impl EncoderApi for HwEncoder {
fn new(cfg: EncoderCfg) -> ResultType<Self>
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<hbb_common::message_proto::Message> {
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<Mutex<Option<String>>> {
HW_ENCODER_NAME.clone()
}
pub fn encode(&mut self, bgra: &[u8]) -> ResultType<Vec<EncodeFrame>> {
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::<EncodeFrame>::new();
data.append(v);
Ok(data)
}
Err(_) => Ok(Vec::<EncodeFrame>::new()),
}
}
}
pub struct HwDecoder {
decoder: Decoder,
pub info: CodecInfo,
}
pub struct HwDecoders {
pub h264: Option<HwDecoder>,
pub h265: Option<HwDecoder>,
}
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<HwDecoder> = None;
let mut h265: Option<HwDecoder> = 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<Self> {
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<Vec<HwDecoderImage>> {
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<u8>, i420: &mut Vec<u8>) -> 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<CodecInfos> {
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()
});
};
}

View File

@ -1,8 +1,9 @@
use crate::common::{ use crate::common::{
wayland, wayland,
x11::{self, Frame}, x11::{self, Frame},
TraitCapturer,
}; };
use std::io; use std::{io, time::Duration};
pub enum Capturer { pub enum Capturer {
X11(x11::Capturer), X11(x11::Capturer),
@ -30,11 +31,20 @@ impl Capturer {
Capturer::WAYLAND(d) => d.height(), Capturer::WAYLAND(d) => d.height(),
} }
} }
}
pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result<Frame<'a>> { impl TraitCapturer for Capturer {
fn set_use_yuv(&mut self, use_yuv: bool) {
match self { match self {
Capturer::X11(d) => d.frame(timeout_ms), Capturer::X11(d) => d.set_use_yuv(use_yuv),
Capturer::WAYLAND(d) => d.frame(timeout_ms), Capturer::WAYLAND(d) => d.set_use_yuv(use_yuv),
}
}
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> {
match self {
Capturer::X11(d) => d.frame(timeout),
Capturer::WAYLAND(d) => d.frame(timeout),
} }
} }
} }
@ -44,32 +54,26 @@ pub enum Display {
WAYLAND(wayland::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 { impl Display {
pub fn primary() -> io::Result<Display> { pub fn primary() -> io::Result<Display> {
Ok(if is_wayland() { Ok(if super::is_x11() {
Display::WAYLAND(wayland::Display::primary()?)
} else {
Display::X11(x11::Display::primary()?) Display::X11(x11::Display::primary()?)
} else {
Display::WAYLAND(wayland::Display::primary()?)
}) })
} }
pub fn all() -> io::Result<Vec<Display>> { pub fn all() -> io::Result<Vec<Display>> {
Ok(if is_wayland() { Ok(if super::is_x11() {
wayland::Display::all()?
.drain(..)
.map(|x| Display::WAYLAND(x))
.collect()
} else {
x11::Display::all()? x11::Display::all()?
.drain(..) .drain(..)
.map(|x| Display::X11(x)) .map(|x| Display::X11(x))
.collect() .collect()
} else {
wayland::Display::all()?
.drain(..)
.map(|x| Display::WAYLAND(x))
.collect()
}) })
} }

View File

@ -1,4 +1,4 @@
pub use self::codec::*; pub use self::vpxcodec::*;
cfg_if! { cfg_if! {
if #[cfg(quartz)] { if #[cfg(quartz)] {
@ -11,6 +11,7 @@ cfg_if! {
mod wayland; mod wayland;
mod x11; mod x11;
pub use self::linux::*; pub use self::linux::*;
pub use self::x11::Frame;
} else { } else {
mod x11; mod x11;
pub use self::x11::*; pub use self::x11::*;
@ -29,8 +30,12 @@ cfg_if! {
pub mod codec; pub mod codec;
mod convert; mod convert;
#[cfg(feature = "hwcodec")]
pub mod hwcodec;
pub mod vpxcodec;
pub use self::convert::*; pub use self::convert::*;
pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller 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; mod vpx;
@ -44,3 +49,19 @@ pub fn would_block_if_equal(old: &mut Vec<u128>, b: &[u8]) -> std::io::Result<()
old.copy_from_slice(b); old.copy_from_slice(b);
Ok(()) 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<Frame<'a>>;
#[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()
}

View File

@ -50,8 +50,14 @@ impl Capturer {
pub fn height(&self) -> usize { pub fn height(&self) -> usize {
self.inner.height() self.inner.height()
} }
}
pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result<Frame<'a>> { 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<Frame<'a>> {
match self.frame.try_lock() { match self.frame.try_lock() {
Ok(mut handle) => { Ok(mut handle) => {
let mut frame = None; let mut frame = None;

View File

@ -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<T> = std::result::Result<T, Error>;
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<Self>
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 encoderate 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<Message> {
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<EncodeFrames> {
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<EncodeFrames> {
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<EncodedVideoFrame>) -> 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<Self::Item> {
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<Self> {
// 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<Vec<u8>> {
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<DecodeFrames> {
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<DecodeFrames> {
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<Self::Item> {
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<u8>) {
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 {}

View File

@ -1,6 +1,6 @@
use crate::common::x11::Frame; use crate::common::{x11::Frame, TraitCapturer};
use crate::wayland::{capturable::*, *}; use crate::wayland::{capturable::*, *};
use std::io; use std::{io, time::Duration};
pub struct Capturer(Display, Box<dyn Recorder>, bool, Vec<u8>); pub struct Capturer(Display, Box<dyn Recorder>, bool, Vec<u8>);
@ -21,9 +21,15 @@ impl Capturer {
pub fn height(&self) -> usize { pub fn height(&self) -> usize {
self.0.height() self.0.height()
} }
}
pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result<Frame<'a>> { impl TraitCapturer for Capturer {
match self.1.capture(timeout_ms as _).map_err(map_err)? { fn set_use_yuv(&mut self, use_yuv: bool) {
self.2 = use_yuv;
}
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> {
match self.1.capture(timeout.as_millis() as _).map_err(map_err)? {
PixelProvider::BGR0(w, h, x) => Ok(Frame(if self.2 { PixelProvider::BGR0(w, h, x) => Ok(Frame(if self.2 {
crate::common::bgra_to_i420(w as _, h as _, &x, &mut self.3); crate::common::bgra_to_i420(w as _, h as _, &x, &mut self.3);
&self.3[..] &self.3[..]

View File

@ -1,5 +1,5 @@
use crate::x11; use crate::{x11, common::TraitCapturer};
use std::{io, ops}; use std::{io, ops, time::Duration};
pub struct Capturer(x11::Capturer); pub struct Capturer(x11::Capturer);
@ -15,8 +15,14 @@ impl Capturer {
pub fn height(&self) -> usize { pub fn height(&self) -> usize {
self.0.display().rect().h as usize self.0.display().rect().h as usize
} }
}
pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result<Frame<'a>> { 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<Frame<'a>> {
Ok(Frame(self.0.frame()?)) Ok(Frame(self.0.frame()?))
} }
} }

View File

@ -282,7 +282,11 @@ impl CapturerMag {
let y = GetSystemMetrics(SM_YVIRTUALSCREEN); let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
let w = GetSystemMetrics(SM_CXVIRTUALSCREEN); let w = GetSystemMetrics(SM_CXVIRTUALSCREEN);
let h = GetSystemMetrics(SM_CYVIRTUALSCREEN); 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( return Err(Error::new(
ErrorKind::Other, ErrorKind::Other,
format!( format!(
@ -442,6 +446,10 @@ impl CapturerMag {
Ok(s) 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<bool> { pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
let name_c = CString::new(name).unwrap(); let name_c = CString::new(name).unwrap();
unsafe { unsafe {
@ -510,10 +518,10 @@ impl CapturerMag {
let y = GetSystemMetrics(SM_YVIRTUALSCREEN); let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
let w = GetSystemMetrics(SM_CXVIRTUALSCREEN); let w = GetSystemMetrics(SM_CXVIRTUALSCREEN);
let h = GetSystemMetrics(SM_CYVIRTUALSCREEN); let h = GetSystemMetrics(SM_CYVIRTUALSCREEN);
if !(self.rect.left == x as _ if !(self.rect.left == x as i32
&& self.rect.top == y as _ && self.rect.top == y as i32
&& self.rect.right == (x + w) as _ && self.rect.right == (x + w) as i32
&& self.rect.bottom == (y + h) as _) && self.rect.bottom == (y + h) as i32)
{ {
return Err(Error::new( return Err(Error::new(
ErrorKind::Other, ErrorKind::Other,

View File

@ -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 { pub fn is_gdi(&self) -> bool {
self.gdi_capturer.is_some() self.gdi_capturer.is_some()
} }

View File

@ -74,6 +74,10 @@ impl Capturer {
Ok(c) Ok(c)
} }
pub fn set_use_yuv(&mut self, use_yuv: bool) {
self.use_yuv = use_yuv;
}
pub fn display(&self) -> &Display { pub fn display(&self) -> &Display {
&self.display &self.display
} }

View File

@ -1,31 +1,177 @@
from pynput.keyboard import Key, Controller from pynput.keyboard import Key, Controller
from pynput.keyboard._xorg import KeyCode from pynput.keyboard._xorg import KeyCode
from pynput._util.xorg import display_manager from pynput._util.xorg import display_manager
import Xlib
from pynput._util.xorg import *
import Xlib
import os import os
import sys import sys
import socket import socket
from Xlib.ext.xtest import fake_input
from Xlib import X
import Xlib
KeyCode._from_symbol("\0") # test 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): class MyController(Controller):
def _handle(self, key, is_press): def _update_keyboard_mapping(self):
"""Resolves a key identifier and sends a keyboard event. """Updates the keyboard mapping.
:param event: The *X* keyboard event.
:param int keysym: The keysym to handle.
""" """
keysym = self._keysym(key) with display_manager(self._display) as dm:
keycode = self._display.keysym_to_keycode(keysym) 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: with display_manager(self._display) as dm:
Xlib.ext.xtest.fake_input( Xlib.ext.xtest.fake_input(
dm, dm,
Xlib.X.KeyPress if is_press else Xlib.X.KeyRelease, Xlib.X.KeyPress if is_press else Xlib.X.KeyRelease,
keycode) 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() keyboard = MyController()
@ -82,7 +228,7 @@ def loop():
else: else:
keyboard.release(name) keyboard.release(name)
except Exception as e: except Exception as e:
print(e) print('[x] error key',e)
loop() loop()

View File

@ -47,7 +47,7 @@ case "$1" in
;; ;;
2) 2)
# for upgrade # for upgrade
service rustdesk stop || true systemctl stop rustdesk || true
;; ;;
esac esac
@ -61,10 +61,26 @@ systemctl start rustdesk
update-desktop-database update-desktop-database
%preun %preun
systemctl stop rustdesk || true case "$1" in
systemctl disable rustdesk || true 0)
rm /etc/systemd/system/rustdesk.service || true # for uninstall
systemctl stop rustdesk || true
systemctl disable rustdesk || true
rm /etc/systemd/system/rustdesk.service || true
;;
1)
# for upgrade
;;
esac
%postun %postun
rm /usr/share/applications/rustdesk.desktop || true case "$1" in
update-desktop-database 0)
# for uninstall
rm /usr/share/applications/rustdesk.desktop || true
update-desktop-database
;;
1)
# for upgrade
;;
esac

View File

@ -48,7 +48,7 @@ case "$1" in
;; ;;
2) 2)
# for upgrade # for upgrade
service rustdesk stop || true systemctl stop rustdesk || true
;; ;;
esac esac
@ -62,10 +62,26 @@ systemctl start rustdesk
update-desktop-database update-desktop-database
%preun %preun
systemctl stop rustdesk || true case "$1" in
systemctl disable rustdesk || true 0)
rm /etc/systemd/system/rustdesk.service || true # for uninstall
systemctl stop rustdesk || true
systemctl disable rustdesk || true
rm /etc/systemd/system/rustdesk.service || true
;;
1)
# for upgrade
;;
esac
%postun %postun
rm /usr/share/applications/rustdesk.desktop || true case "$1" in
update-desktop-database 0)
# for uninstall
rm /usr/share/applications/rustdesk.desktop || true
update-desktop-database
;;
1)
# for upgrade
;;
esac

2
rust-toolchain.toml Normal file
View File

@ -0,0 +1,2 @@
[toolchain]
channel = "1.62.0"

View File

@ -6,7 +6,7 @@ After=systemd-user-sessions.service
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/bin/rustdesk --service ExecStart=/usr/bin/rustdesk --service
PIDFile=/var/run/rustdesk.pid PIDFile=/run/rustdesk.pid
KillMode=mixed KillMode=mixed
TimeoutStopSec=30 TimeoutStopSec=30
User=root User=root

15
rustdesk.service.user Normal file
View File

@ -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

View File

@ -24,6 +24,7 @@ use hbb_common::{
log, log,
message_proto::{option_message::BoolOption, *}, message_proto::{option_message::BoolOption, *},
protobuf::Message as _, protobuf::Message as _,
rand,
rendezvous_proto::*, rendezvous_proto::*,
socket_client, socket_client,
sodiumoxide::crypto::{box_, secretbox, sign}, sodiumoxide::crypto::{box_, secretbox, sign},
@ -32,7 +33,12 @@ use hbb_common::{
AddrMangle, ResultType, Stream, AddrMangle, ResultType, Stream,
}; };
pub use helper::LatencyController; 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::*; pub use super::lang::*;
@ -149,11 +155,25 @@ impl Client {
true, true,
)); ));
} }
let rendezvous_server = crate::get_rendezvous_server(1_000).await; let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await;
log::info!("rendezvous server: {}", rendezvous_server);
let mut socket = 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 my_addr = socket.local_addr();
let mut signed_id_pk = Vec::new(); let mut signed_id_pk = Vec::new();
let mut relay_server = "".to_owned(); let mut relay_server = "".to_owned();
@ -166,7 +186,7 @@ impl Client {
for i in 1..=3 { for i in 1..=3 {
log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer); log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer);
let mut msg_out = RendezvousMessage::new(); 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); let nat_type = NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT);
msg_out.set_punch_hole_request(PunchHoleRequest { msg_out.set_punch_hole_request(PunchHoleRequest {
id: peer.to_owned(), id: peer.to_owned(),
@ -180,7 +200,7 @@ impl Client {
if let Some(Ok(bytes)) = socket.next_timeout(i * 6000).await { if let Some(Ok(bytes)) = socket.next_timeout(i * 6000).await {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
match msg_in.union { 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.socket_addr.is_empty() {
if !ph.other_failure.is_empty() { if !ph.other_failure.is_empty() {
bail!(ph.other_failure); bail!(ph.other_failure);
@ -200,22 +220,22 @@ impl Client {
} }
} }
} else { } else {
peer_nat_type = ph.get_nat_type(); peer_nat_type = ph.nat_type();
is_local = ph.get_is_local(); is_local = ph.is_local();
signed_id_pk = ph.pk; signed_id_pk = ph.pk.into();
relay_server = ph.relay_server; relay_server = ph.relay_server;
peer_addr = AddrMangle::decode(&ph.socket_addr); peer_addr = AddrMangle::decode(&ph.socket_addr);
log::info!("Hole Punched {} = {}", peer, peer_addr); log::info!("Hole Punched {} = {}", peer, peer_addr);
break; break;
} }
} }
Some(rendezvous_message::Union::relay_response(rr)) => { Some(rendezvous_message::Union::RelayResponse(rr)) => {
log::info!( log::info!(
"relay requested from peer, time used: {:?}, relay_server: {}", "relay requested from peer, time used: {:?}, relay_server: {}",
start.elapsed(), start.elapsed(),
rr.relay_server rr.relay_server
); );
signed_id_pk = rr.get_pk().into(); signed_id_pk = rr.pk().into();
let mut conn = let mut conn =
Self::create_relay(peer, rr.uuid, rr.relay_server, key, conn_type) Self::create_relay(peer, rr.uuid, rr.relay_server, key, conn_type)
.await?; .await?;
@ -359,7 +379,7 @@ impl Client {
conn: &mut Stream, conn: &mut Stream,
) -> ResultType<()> { ) -> ResultType<()> {
let rs_pk = get_rs_pk(if key.is_empty() { let rs_pk = get_rs_pk(if key.is_empty() {
"OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" hbb_common::config::RS_PUB_KEY
} else { } else {
key key
}); });
@ -386,7 +406,7 @@ impl Client {
Some(res) => { Some(res) => {
let bytes = res?; let bytes = res?;
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { 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 let Ok((id, their_pk_b)) = decode_id_pk(&si.id, &sign_pk) {
if id == peer_id { if id == peer_id {
let their_pk_b = box_::PublicKey(their_pk_b); 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 sealed_key = box_::seal(&key.0, &nonce, &their_pk_b, &out_sk_b);
let mut msg_out = Message::new(); let mut msg_out = Message::new();
msg_out.set_public_key(PublicKey { msg_out.set_public_key(PublicKey {
asymmetric_value: our_pk_b.0.into(), asymmetric_value: Vec::from(our_pk_b.0).into(),
symmetric_value: sealed_key, symmetric_value: sealed_key.into(),
..Default::default() ..Default::default()
}); });
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??; timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
@ -470,7 +490,7 @@ impl Client {
socket.send(&msg_out).await?; socket.send(&msg_out).await?;
if let Some(Ok(bytes)) = socket.next_timeout(CONNECT_TIMEOUT).await { if let Some(Ok(bytes)) = socket.next_timeout(CONNECT_TIMEOUT).await {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { 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() { if !rs.refuse_reason.is_empty() {
bail!(rs.refuse_reason); bail!(rs.refuse_reason);
} }
@ -736,7 +756,12 @@ impl VideoHandler {
/// Create a new video handler. /// Create a new video handler.
pub fn new(latency_controller: Arc<Mutex<LatencyController>>) -> Self { pub fn new(latency_controller: Arc<Mutex<LatencyController>>) -> Self {
VideoHandler { 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, latency_controller,
rgb: Default::default(), rgb: Default::default(),
} }
@ -752,35 +777,40 @@ impl VideoHandler {
.update_video(vf.timestamp); .update_video(vf.timestamp);
} }
match &vf.union { 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), _ => Ok(false),
} }
} }
/// Handle a VP9S frame. /// Handle a VP9S frame.
pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType<bool> { // pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType<bool> {
let mut last_frame = Image::new(); // let mut last_frame = Image::new();
for vp9 in vp9s.frames.iter() { // for vp9 in vp9s.frames.iter() {
for frame in self.decoder.decode(&vp9.data)? { // for frame in self.decoder.decode(&vp9.data)? {
drop(last_frame); // drop(last_frame);
last_frame = frame; // last_frame = frame;
} // }
} // }
for frame in self.decoder.flush()? { // for frame in self.decoder.flush()? {
drop(last_frame); // drop(last_frame);
last_frame = frame; // last_frame = frame;
} // }
if last_frame.is_null() { // if last_frame.is_null() {
Ok(false) // Ok(false)
} else { // } else {
last_frame.rgb(1, true, &mut self.rgb); // last_frame.rgb(1, true, &mut self.rgb);
Ok(true) // Ok(true)
} // }
} // }
/// Reset the decoder. /// Reset the decoder.
pub fn reset(&mut self) { 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 version: i64,
pub conn_id: i32, pub conn_id: i32,
features: Option<Features>, features: Option<Features>,
session_id: u64,
pub supported_encoding: Option<(bool, bool)>,
pub restarting_remote_device: bool,
} }
impl Deref for LoginConfigHandler { impl Deref for LoginConfigHandler {
@ -833,6 +866,9 @@ impl LoginConfigHandler {
let config = self.load_config(); let config = self.load_config();
self.remember = !config.password.is_empty(); self.remember = !config.password.is_empty();
self.config = config; self.config = config;
self.session_id = rand::random();
self.supported_encoding = None;
self.restarting_remote_device = false;
} }
/// Check if the client should auto login. /// Check if the client should auto login.
@ -946,6 +982,8 @@ impl LoginConfigHandler {
option.block_input = BoolOption::Yes.into(); option.block_input = BoolOption::Yes.into();
} else if name == "unblock-input" { } else if name == "unblock-input" {
option.block_input = BoolOption::No.into(); option.block_input = BoolOption::No.into();
} else if name == "show-quality-monitor" {
config.show_quality_monitor = !config.show_quality_monitor;
} else { } else {
let v = self.options.get(&name).is_some(); let v = self.options.get(&name).is_some();
if v { if v {
@ -984,15 +1022,8 @@ impl LoginConfigHandler {
n += 1; n += 1;
} else if q == "custom" { } else if q == "custom" {
let config = PeerConfig::load(&self.id); let config = PeerConfig::load(&self.id);
let mut it = config.custom_image_quality.iter(); msg.custom_image_quality = config.custom_image_quality[0] << 8;
let bitrate = it.next(); n += 1;
let quantizer = it.next();
if let Some(bitrate) = bitrate {
if let Some(quantizer) = quantizer {
msg.custom_image_quality = bitrate << 8 | quantizer;
n += 1;
}
}
} }
if self.get_toggle_option("show-remote-cursor") { if self.get_toggle_option("show-remote-cursor") {
msg.show_remote_cursor = BoolOption::Yes.into(); msg.show_remote_cursor = BoolOption::Yes.into();
@ -1018,6 +1049,10 @@ impl LoginConfigHandler {
msg.disable_clipboard = BoolOption::Yes.into(); msg.disable_clipboard = BoolOption::Yes.into();
n += 1; 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 { if n > 0 {
Some(msg) Some(msg)
} else { } else {
@ -1066,6 +1101,8 @@ impl LoginConfigHandler {
self.config.disable_audio self.config.disable_audio
} else if name == "disable-clipboard" { } else if name == "disable-clipboard" {
self.config.disable_clipboard self.config.disable_clipboard
} else if name == "show-quality-monitor" {
self.config.show_quality_monitor
} else { } else {
!self.get_option(name).is_empty() !self.get_option(name).is_empty()
} }
@ -1094,17 +1131,17 @@ impl LoginConfigHandler {
/// ///
/// * `bitrate` - The given bitrate. /// * `bitrate` - The given bitrate.
/// * `quantizer` - The given quantizer. /// * `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(); let mut misc = Misc::new();
misc.set_option(OptionMessage { misc.set_option(OptionMessage {
custom_image_quality: bitrate << 8 | quantizer, custom_image_quality: image_quality << 8,
..Default::default() ..Default::default()
}); });
let mut msg_out = Message::new(); let mut msg_out = Message::new();
msg_out.set_misc(misc); msg_out.set_misc(misc);
let mut config = self.load_config(); let mut config = self.load_config();
config.image_quality = "custom".to_owned(); 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); self.save_config(config);
msg_out msg_out
} }
@ -1202,6 +1239,10 @@ impl LoginConfigHandler {
self.conn_id = pi.conn_id; self.conn_id = pi.conn_id;
// no matter if change, for update file time // no matter if change, for update file time
self.save_config(config); self.save_config(config);
#[cfg(feature = "hwcodec")]
{
self.supported_encoding = Some((pi.encoding.h264, pi.encoding.h265));
}
} }
pub fn get_remote_dir(&self) -> String { pub fn get_remote_dir(&self) -> String {
@ -1231,10 +1272,11 @@ impl LoginConfigHandler {
let my_id = Config::get_id(); let my_id = Config::get_id();
let mut lr = LoginRequest { let mut lr = LoginRequest {
username: self.id.clone(), username: self.id.clone(),
password, password: password.into(),
my_id, my_id,
my_name: crate::username(), my_name: crate::username(),
option: self.get_option_message(true).into(), option: self.get_option_message(true).into(),
session_id: self.session_id,
..Default::default() ..Default::default()
}; };
if self.is_file_transfer { if self.is_file_transfer {
@ -1254,6 +1296,26 @@ impl LoginConfigHandler {
msg_out.set_login_request(lr); msg_out.set_login_request(lr);
msg_out 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. /// Media data.
@ -1282,9 +1344,11 @@ where
let latency_controller = LatencyController::new(); let latency_controller = LatencyController::new();
let latency_controller_cl = latency_controller.clone(); 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 || { std::thread::spawn(move || {
let mut video_handler = VideoHandler::new(latency_controller);
loop { loop {
if let Ok(data) = video_receiver.recv() { if let Ok(data) = video_receiver.recv() {
match data { match data {
@ -1459,11 +1523,21 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) {
/// * `peer` - [`Stream`] for communicating with peer. /// * `peer` - [`Stream`] for communicating with peer.
pub async fn handle_hash( pub async fn handle_hash(
lc: Arc<RwLock<LoginConfigHandler>>, lc: Arc<RwLock<LoginConfigHandler>>,
password_preset: &str,
hash: Hash, hash: Hash,
interface: &impl Interface, interface: &impl Interface,
peer: &mut Stream, peer: &mut Stream,
) { ) {
let mut password = lc.read().unwrap().password.clone(); 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() { if password.is_empty() {
password = lc.read().unwrap().config.password.clone(); 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 msgbox(&self, msgtype: &str, title: &str, text: &str);
fn handle_login_error(&mut self, err: &str) -> bool; fn handle_login_error(&mut self, err: &str) -> bool;
fn handle_peer_info(&mut self, pi: PeerInfo); 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_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream);
async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream);
} }

View File

@ -3,7 +3,7 @@ use std::{
time::Instant, time::Instant,
}; };
use hbb_common::log; use hbb_common::{log, message_proto::{VideoFrame, video_frame}};
const MAX_LATENCY: i64 = 500; const MAX_LATENCY: i64 = 500;
const MIN_LATENCY: i64 = 100; const MIN_LATENCY: i64 = 100;
@ -59,3 +59,33 @@ impl LatencyController {
self.allow_audio 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(),
}
}
}

View File

@ -17,8 +17,8 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
}); });
} }
Message { Message {
union: Some(message::Union::cliprdr(Cliprdr { union: Some(message::Union::Cliprdr(Cliprdr {
union: Some(cliprdr::Union::format_list(CliprdrServerFormatList { union: Some(cliprdr::Union::FormatList(CliprdrServerFormatList {
conn_id, conn_id,
formats, formats,
..Default::default() ..Default::default()
@ -29,8 +29,8 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
} }
} }
ClipbaordFile::ServerFormatListResponse { conn_id, msg_flags } => Message { ClipbaordFile::ServerFormatListResponse { conn_id, msg_flags } => Message {
union: Some(message::Union::cliprdr(Cliprdr { union: Some(message::Union::Cliprdr(Cliprdr {
union: Some(cliprdr::Union::format_list_response( union: Some(cliprdr::Union::FormatListResponse(
CliprdrServerFormatListResponse { CliprdrServerFormatListResponse {
conn_id, conn_id,
msg_flags, msg_flags,
@ -45,8 +45,8 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
conn_id, conn_id,
requested_format_id, requested_format_id,
} => Message { } => Message {
union: Some(message::Union::cliprdr(Cliprdr { union: Some(message::Union::Cliprdr(Cliprdr {
union: Some(cliprdr::Union::format_data_request( union: Some(cliprdr::Union::FormatDataRequest(
CliprdrServerFormatDataRequest { CliprdrServerFormatDataRequest {
conn_id, conn_id,
requested_format_id, requested_format_id,
@ -62,12 +62,12 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
msg_flags, msg_flags,
format_data, format_data,
} => Message { } => Message {
union: Some(message::Union::cliprdr(Cliprdr { union: Some(message::Union::Cliprdr(Cliprdr {
union: Some(cliprdr::Union::format_data_response( union: Some(cliprdr::Union::FormatDataResponse(
CliprdrServerFormatDataResponse { CliprdrServerFormatDataResponse {
conn_id, conn_id,
msg_flags, msg_flags,
format_data, format_data: format_data.into(),
..Default::default() ..Default::default()
}, },
)), )),
@ -86,8 +86,8 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
have_clip_data_id, have_clip_data_id,
clip_data_id, clip_data_id,
} => Message { } => Message {
union: Some(message::Union::cliprdr(Cliprdr { union: Some(message::Union::Cliprdr(Cliprdr {
union: Some(cliprdr::Union::file_contents_request( union: Some(cliprdr::Union::FileContentsRequest(
CliprdrFileContentsRequest { CliprdrFileContentsRequest {
conn_id, conn_id,
stream_id, stream_id,
@ -111,13 +111,13 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
stream_id, stream_id,
requested_data, requested_data,
} => Message { } => Message {
union: Some(message::Union::cliprdr(Cliprdr { union: Some(message::Union::Cliprdr(Cliprdr {
union: Some(cliprdr::Union::file_contents_response( union: Some(cliprdr::Union::FileContentsResponse(
CliprdrFileContentsResponse { CliprdrFileContentsResponse {
conn_id, conn_id,
msg_flags, msg_flags,
stream_id, stream_id,
requested_data, requested_data: requested_data.into(),
..Default::default() ..Default::default()
}, },
)), )),
@ -130,7 +130,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message {
pub fn msg_2_clip(msg: Cliprdr) -> Option<ClipbaordFile> { pub fn msg_2_clip(msg: Cliprdr) -> Option<ClipbaordFile> {
match msg.union { match msg.union {
Some(cliprdr::Union::format_list(data)) => { Some(cliprdr::Union::FormatList(data)) => {
let mut format_list: Vec<(i32, String)> = Vec::new(); let mut format_list: Vec<(i32, String)> = Vec::new();
for v in data.formats.iter() { for v in data.formats.iter() {
format_list.push((v.id, v.format.clone())); format_list.push((v.id, v.format.clone()));
@ -140,26 +140,26 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option<ClipbaordFile> {
format_list, format_list,
}) })
} }
Some(cliprdr::Union::format_list_response(data)) => { Some(cliprdr::Union::FormatListResponse(data)) => {
Some(ClipbaordFile::ServerFormatListResponse { Some(ClipbaordFile::ServerFormatListResponse {
conn_id: data.conn_id, conn_id: data.conn_id,
msg_flags: data.msg_flags, msg_flags: data.msg_flags,
}) })
} }
Some(cliprdr::Union::format_data_request(data)) => { Some(cliprdr::Union::FormatDataRequest(data)) => {
Some(ClipbaordFile::ServerFormatDataRequest { Some(ClipbaordFile::ServerFormatDataRequest {
conn_id: data.conn_id, conn_id: data.conn_id,
requested_format_id: data.requested_format_id, requested_format_id: data.requested_format_id,
}) })
} }
Some(cliprdr::Union::format_data_response(data)) => { Some(cliprdr::Union::FormatDataResponse(data)) => {
Some(ClipbaordFile::ServerFormatDataResponse { Some(ClipbaordFile::ServerFormatDataResponse {
conn_id: data.conn_id, conn_id: data.conn_id,
msg_flags: data.msg_flags, 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 { Some(ClipbaordFile::FileContentsRequest {
conn_id: data.conn_id, conn_id: data.conn_id,
stream_id: data.stream_id, stream_id: data.stream_id,
@ -172,12 +172,12 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option<ClipbaordFile> {
clip_data_id: data.clip_data_id, clip_data_id: data.clip_data_id,
}) })
} }
Some(cliprdr::Union::file_contents_response(data)) => { Some(cliprdr::Union::FileContentsResponse(data)) => {
Some(ClipbaordFile::FileContentsResponse { Some(ClipbaordFile::FileContentsResponse {
conn_id: data.conn_id, conn_id: data.conn_id,
msg_flags: data.msg_flags, msg_flags: data.msg_flags,
stream_id: data.stream_id, stream_id: data.stream_id,
requested_data: data.requested_data, requested_data: data.requested_data.into(),
}) })
} }
_ => None, _ => None,

View File

@ -11,8 +11,8 @@ use hbb_common::{
config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT},
get_version_number, log, get_version_number, log,
message_proto::*, message_proto::*,
protobuf::Enum,
protobuf::Message as _, protobuf::Message as _,
protobuf::ProtobufEnum,
rendezvous_proto::*, rendezvous_proto::*,
sleep, socket_client, tokio, ResultType, sleep, socket_client, tokio, ResultType,
}; };
@ -34,7 +34,7 @@ lazy_static::lazy_static! {
#[inline] #[inline]
pub fn valid_for_numlock(evt: &KeyEvent) -> bool { 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(); let v = ck.value();
(v >= ControlKey::Numpad0.value() && v <= ControlKey::Numpad9.value()) (v >= ControlKey::Numpad0.value() && v <= ControlKey::Numpad9.value())
|| v == ControlKey::Decimal.value() || v == ControlKey::Decimal.value()
@ -51,7 +51,7 @@ pub fn create_clipboard_msg(content: String) -> Message {
let mut msg = Message::new(); let mut msg = Message::new();
msg.set_clipboard(Clipboard { msg.set_clipboard(Clipboard {
compress, compress,
content, content: content.into(),
..Default::default() ..Default::default()
}); });
msg msg
@ -82,7 +82,7 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc<Mutex<String>>>)
let content = if clipboard.compress { let content = if clipboard.compress {
decompress(&clipboard.content) decompress(&clipboard.content)
} else { } else {
clipboard.content clipboard.content.into()
}; };
if let Ok(content) = String::from_utf8(content) { if let Ok(content) = String::from_utf8(content) {
if content.is_empty() { if content.is_empty() {
@ -249,7 +249,7 @@ async fn test_nat_type_() -> ResultType<bool> {
return Ok(true); return Ok(true);
} }
let start = std::time::Instant::now(); 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 server1 = rendezvous_server;
let tmp: Vec<&str> = server1.split(":").collect(); let tmp: Vec<&str> = server1.split(":").collect();
if tmp.len() != 2 { if tmp.len() != 2 {
@ -286,7 +286,7 @@ async fn test_nat_type_() -> ResultType<bool> {
socket.send(&msg_out).await?; socket.send(&msg_out).await?;
if let Some(Ok(bytes)) = socket.next_timeout(RENDEZVOUS_TIMEOUT).await { if let Some(Ok(bytes)) = socket.next_timeout(RENDEZVOUS_TIMEOUT).await {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { 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 { if i == 0 {
port1 = tnr.port; port1 = tnr.port;
} else { } else {
@ -318,21 +318,53 @@ async fn test_nat_type_() -> ResultType<bool> {
Ok(ok) Ok(ok)
} }
#[cfg(any(target_os = "android", target_os = "ios"))] pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec<String>, bool) {
pub async fn get_rendezvous_server(_ms_timeout: u64) -> String { #[cfg(any(target_os = "android", target_os = "ios"))]
Config::get_rendezvous_server() 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<String> = 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<String>) {
(
Config::get_rendezvous_server(),
Config::get_rendezvous_servers(),
)
}
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[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<String>) {
crate::ipc::get_rendezvous_server(ms_timeout).await crate::ipc::get_rendezvous_server(ms_timeout).await
} }
#[inline]
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
pub async fn get_nat_type(_ms_timeout: u64) -> i32 { pub async fn get_nat_type(_ms_timeout: u64) -> i32 {
Config::get_nat_type() Config::get_nat_type()
} }
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub async fn get_nat_type(ms_timeout: u64) -> i32 { pub async fn get_nat_type(ms_timeout: u64) -> i32 {
crate::ipc::get_nat_type(ms_timeout).await 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")] #[tokio::main(flavor = "current_thread")]
async fn test_rendezvous_server_() { async fn test_rendezvous_server_() {
let servers = Config::get_rendezvous_servers(); let servers = Config::get_rendezvous_servers();
hbb_common::config::ONLINE.lock().unwrap().clear(); Config::reset_online();
let mut futs = Vec::new(); let mut futs = Vec::new();
for host in servers { for host in servers {
futs.push(tokio::spawn(async move { futs.push(tokio::spawn(async move {
@ -370,6 +402,17 @@ pub fn test_rendezvous_server() {
std::thread::spawn(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] #[inline]
pub fn get_time() -> i64 { pub fn get_time() -> i64 {
std::time::SystemTime::now() std::time::SystemTime::now()
@ -414,7 +457,7 @@ pub const POSTFIX_SERVICE: &'static str = "_service";
#[inline] #[inline]
pub fn is_control_key(evt: &KeyEvent, key: &ControlKey) -> bool { 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() ck.value() == key.value()
} else { } else {
false false
@ -423,7 +466,7 @@ pub fn is_control_key(evt: &KeyEvent, key: &ControlKey) -> bool {
#[inline] #[inline]
pub fn is_modifier(evt: &KeyEvent) -> bool { 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(); let v = ck.value();
v == ControlKey::Alt.value() v == ControlKey::Alt.value()
|| v == ControlKey::Shift.value() || v == ControlKey::Shift.value()
@ -439,14 +482,15 @@ pub fn is_modifier(evt: &KeyEvent) -> bool {
} }
pub fn check_software_update() { 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")] #[tokio::main(flavor = "current_thread")]
async fn _check_software_update() -> hbb_common::ResultType<()> { async fn check_software_update_() -> hbb_common::ResultType<()> {
sleep(3.).await; 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 = let mut socket =
socket_client::new_udp(Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT).await?; 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; use hbb_common::protobuf::Message;
if let Some(Ok((bytes, _))) = socket.next_timeout(30_000).await { if let Some(Ok((bytes, _))) = socket.next_timeout(30_000).await {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { 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); let version = hbb_common::get_version_from_url(&su.url);
if get_version_number(&version) > get_version_number(crate::VERSION) { if get_version_number(&version) > get_version_number(crate::VERSION) {
*SOFTWARE_UPDATE_URL.lock().unwrap() = su.url; *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") name.to_lowercase().ends_with("setdown.exe") || name.to_lowercase().ends_with("安装.exe")
} }
pub fn get_uuid() -> Vec<u8> {
#[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 { pub fn get_custom_rendezvous_server(custom: String) -> String {
if !custom.is_empty() { if !custom.is_empty() {
return custom; return custom;

View File

@ -590,8 +590,8 @@ impl Interface for Session {
} }
} }
async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream) { async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) {
handle_hash(self.lc.clone(), hash, self, peer).await; 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) { 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); log::debug!("Exit io_loop of id={}", session.id);
} }
Err(err) => { Err(err) => {
crate::common::test_rendezvous_server();
session.msgbox("error", "Connection Error", &err.to_string()); 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 { async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool {
if let Ok(msg_in) = Message::parse_from_bytes(&data) { if let Ok(msg_in) = Message::parse_from_bytes(&data) {
match msg_in.union { match msg_in.union {
Some(message::Union::video_frame(vf)) => { Some(message::Union::VideoFrame(vf)) => {
if !self.first_frame { if !self.first_frame {
self.first_frame = true; self.first_frame = true;
} }
@ -733,26 +732,26 @@ impl Connection {
))); )));
} }
} }
Some(message::Union::hash(hash)) => { Some(message::Union::Hash(hash)) => {
self.session.handle_hash(hash, peer).await; self.session.handle_hash("", hash, peer).await;
} }
Some(message::Union::login_response(lr)) => match lr.union { Some(message::Union::LoginResponse(lr)) => match lr.union {
Some(login_response::Union::error(err)) => { Some(login_response::Union::Error(err)) => {
if !self.session.handle_login_error(&err) { if !self.session.handle_login_error(&err) {
return false; return false;
} }
} }
Some(login_response::Union::peer_info(pi)) => { Some(login_response::Union::PeerInfo(pi)) => {
self.session.handle_peer_info(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 { if !self.session.lc.read().unwrap().disable_clipboard {
let content = if cb.compress { let content = if cb.compress {
decompress(&cb.content) decompress(&cb.content)
} else { } else {
cb.content cb.content.into()
}; };
if let Ok(content) = String::from_utf8(content) { if let Ok(content) = String::from_utf8(content) {
self.session 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); let colors = hbb_common::compress::decompress(&cd.colors);
self.session.push_event( self.session.push_event(
"cursor_data", "cursor_data",
@ -777,18 +776,18 @@ impl Connection {
], ],
); );
} }
Some(message::Union::cursor_id(id)) => { Some(message::Union::CursorId(id)) => {
self.session self.session
.push_event("cursor_id", vec![("id", &id.to_string())]); .push_event("cursor_id", vec![("id", &id.to_string())]);
} }
Some(message::Union::cursor_position(cp)) => { Some(message::Union::CursorPosition(cp)) => {
self.session.push_event( self.session.push_event(
"cursor_position", "cursor_position",
vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())],
); );
} }
Some(message::Union::file_response(fr)) => match fr.union { Some(message::Union::FileResponse(fr)) => match fr.union {
Some(file_response::Union::dir(fd)) => { Some(file_response::Union::Dir(fd)) => {
let mut entries = fd.entries.to_vec(); let mut entries = fd.entries.to_vec();
if self.session.peer_platform() == "Windows" { if self.session.peer_platform() == "Windows" {
fs::transform_windows_path(&mut entries); fs::transform_windows_path(&mut entries);
@ -802,7 +801,7 @@ impl Connection {
job.set_files(entries); 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 Some(job) = fs::get_job(block.id, &mut self.write_jobs) {
if let Err(_err) = job.write(block, None).await { if let Err(_err) = job.write(block, None).await {
// to-do: add "skip" for writing job // to-do: add "skip" for writing job
@ -810,17 +809,17 @@ impl Connection {
self.update_jobs_status(); 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) { if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) {
job.modify_time(); job.modify_time();
fs::remove_job(d.id, &mut self.write_jobs); fs::remove_job(d.id, &mut self.write_jobs);
} }
self.handle_job_status(d.id, d.file_num, None); 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)); 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 digest.is_upload {
if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { 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) { if let Some(file) = job.files().get(digest.file_num as usize) {
@ -831,9 +830,9 @@ impl Connection {
id: digest.id, id: digest.id,
file_num: digest.file_num, file_num: digest.file_num,
union: Some(if overwrite { union: Some(if overwrite {
file_transfer_send_confirm_request::Union::offset_blk(0) file_transfer_send_confirm_request::Union::OffsetBlk(0)
} else { } else {
file_transfer_send_confirm_request::Union::skip( file_transfer_send_confirm_request::Union::Skip(
true, true,
) )
}), }),
@ -863,7 +862,7 @@ impl Connection {
let msg= new_send_confirm(FileTransferSendConfirmRequest { let msg= new_send_confirm(FileTransferSendConfirmRequest {
id: digest.id, id: digest.id,
file_num: digest.file_num, 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() ..Default::default()
}); });
self.session.send_msg(msg); self.session.send_msg(msg);
@ -875,9 +874,9 @@ impl Connection {
id: digest.id, id: digest.id,
file_num: digest.file_num, file_num: digest.file_num,
union: Some(if overwrite { union: Some(if overwrite {
file_transfer_send_confirm_request::Union::offset_blk(0) file_transfer_send_confirm_request::Union::OffsetBlk(0)
} else { } else {
file_transfer_send_confirm_request::Union::skip(true) file_transfer_send_confirm_request::Union::Skip(true)
}), }),
..Default::default() ..Default::default()
}, },
@ -897,7 +896,7 @@ impl Connection {
FileTransferSendConfirmRequest { FileTransferSendConfirmRequest {
id: digest.id, id: digest.id,
file_num: digest.file_num, 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() ..Default::default()
}, },
); );
@ -914,15 +913,15 @@ impl Connection {
} }
_ => {} _ => {}
}, },
Some(message::Union::misc(misc)) => match misc.union { Some(message::Union::Misc(misc)) => match misc.union {
Some(misc::Union::audio_format(f)) => { Some(misc::Union::AudioFormat(f)) => {
self.audio_handler.handle_format(f); // self.audio_handler.handle_format(f); //
} }
Some(misc::Union::chat_message(c)) => { Some(misc::Union::ChatMessage(c)) => {
self.session self.session
.push_event("chat_client_mode", vec![("text", &c.text)]); .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); log::info!("Change permission {:?} -> {}", p.permission, p.enabled);
use permission_info::Permission; use permission_info::Permission;
self.session.push_event( 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.video_handler.reset();
self.session.push_event( self.session.push_event(
"switch_display", "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); self.session.msgbox("error", "Connection Error", &c);
return false; return false;
} }
_ => {} _ => {}
}, },
Some(message::Union::test_delay(t)) => { Some(message::Union::TestDelay(t)) => {
self.session.handle_test_delay(t, peer).await; 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 { if !self.session.lc.read().unwrap().disable_audio {
self.audio_handler.handle_frame(frame); self.audio_handler.handle_frame(frame);
} }
} }
Some(message::Union::file_action(action)) => match action.union { Some(message::Union::FileAction(action)) => match action.union {
Some(file_action::Union::send_confirm(c)) => { Some(file_action::Union::SendConfirm(c)) => {
if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) {
job.confirm(&c); job.confirm(&c);
} }
@ -1155,9 +1154,9 @@ impl Connection {
id, id,
file_num, file_num,
union: if need_override { union: if need_override {
Some(file_transfer_send_confirm_request::Union::offset_blk(0)) Some(file_transfer_send_confirm_request::Union::OffsetBlk(0))
} else { } else {
Some(file_transfer_send_confirm_request::Union::skip(true)) Some(file_transfer_send_confirm_request::Union::Skip(true))
}, },
..Default::default() ..Default::default()
}); });
@ -1173,9 +1172,9 @@ impl Connection {
id, id,
file_num, file_num,
union: if need_override { union: if need_override {
Some(file_transfer_send_confirm_request::Union::offset_blk(0)) Some(file_transfer_send_confirm_request::Union::OffsetBlk(0))
} else { } else {
Some(file_transfer_send_confirm_request::Union::skip(true)) Some(file_transfer_send_confirm_request::Union::Skip(true))
}, },
..Default::default() ..Default::default()
}); });
@ -1441,15 +1440,13 @@ pub mod connection_manager {
Some(Data::Login { Some(Data::Login {
id, id,
is_file_transfer, is_file_transfer,
port_forward,
peer_id, peer_id,
name, name,
authorized, authorized,
keyboard, keyboard,
clipboard, clipboard,
audio, audio,
file, ..
file_transfer_enabled,
}) => { }) => {
current_id = id; current_id = id;
let mut client = Client { let mut client = Client {
@ -1681,7 +1678,7 @@ pub mod connection_manager {
let mut req = FileTransferSendConfirmRequest { let mut req = FileTransferSendConfirmRequest {
id, id,
file_num, 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() ..Default::default()
}; };
let digest = FileTransferDigest { let digest = FileTransferDigest {

View File

@ -7,7 +7,7 @@ use std::{
use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer};
use serde_json::{json, Number, Value}; use serde_json::{json, Number, Value};
use hbb_common::ResultType; use hbb_common::{ResultType, password_security};
use hbb_common::{ use hbb_common::{
config::{self, Config, LocalConfig, PeerConfig, ONLINE}, config::{self, Config, LocalConfig, PeerConfig, ONLINE},
fs, log, 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_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_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, 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) { 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" => { "server_id" => {
res = ui_interface::get_id(); res = ui_interface::get_id();
} }
"server_password" => { "temporary_password" => {
res = Config::get_password(); res = password_security::temporary_password();
} }
"connect_statue" => { "connect_statue" => {
res = ONLINE res = ONLINE
@ -627,7 +628,7 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co
} }
} }
"uuid" => { "uuid" => {
res = base64::encode(crate::get_uuid()); res = base64::encode(get_uuid());
} }
_ => { _ => {
log::error!("Unknown name of get_by_name: {}", name); 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 // Server Side
"update_password" => { // "update_password" => {
if value.is_empty() { // if value.is_empty() {
Config::set_password(&Config::get_auto_password()); // Config::set_password(&Config::get_auto_password());
} else { // } else {
Config::set_password(value); // Config::set_password(value);
} // }
} // }
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
"chat_server_mode" => { "chat_server_mode" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, Value>>(value) { if let Ok(m) = serde_json::from_str::<HashMap<String, Value>>(value) {

View File

@ -2,6 +2,7 @@ use std::{collections::HashMap, sync::atomic::Ordering};
#[cfg(not(windows))] #[cfg(not(windows))]
use std::{fs::File, io::prelude::*}; use std::{fs::File, io::prelude::*};
use bytes::Bytes;
use parity_tokio_ipc::{ use parity_tokio_ipc::{
Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes,
}; };
@ -15,7 +16,7 @@ use hbb_common::{
config::{self, Config, Config2}, config::{self, Config, Config2},
futures::StreamExt as _, futures::StreamExt as _,
futures_util::sink::SinkExt, futures_util::sink::SinkExt,
log, timeout, tokio, log, password_security as password, timeout, tokio,
tokio::io::{AsyncRead, AsyncWrite}, tokio::io::{AsyncRead, AsyncWrite},
tokio_util::codec::Framed, tokio_util::codec::Framed,
ResultType, ResultType,
@ -66,7 +67,7 @@ pub enum FS {
WriteBlock { WriteBlock {
id: i32, id: i32,
file_num: i32, file_num: i32,
data: Vec<u8>, data: Bytes,
compressed: bool, compressed: bool,
}, },
WriteDone { 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")] #[serde(tag = "t", content = "c")]
pub enum Data { pub enum Data {
@ -102,6 +144,7 @@ pub enum Data {
audio: bool, audio: bool,
file: bool, file: bool,
file_transfer_enabled: bool, file_transfer_enabled: bool,
restart: bool,
}, },
ChatMessage { ChatMessage {
text: String, text: String,
@ -130,6 +173,15 @@ pub enum Data {
ClipbaordFile(ClipbaordFile), ClipbaordFile(ClipbaordFile),
ClipboardFileEnabled(bool), ClipboardFileEnabled(bool),
PrivacyModeState((i32, PrivacyModeState)), 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")] #[tokio::main(flavor = "current_thread")]
@ -284,12 +336,18 @@ async fn handle(data: Data, stream: &mut Connection) {
let value; let value;
if name == "id" { if name == "id" {
value = Some(Config::get_id()); value = Some(Config::get_id());
} else if name == "password" { } else if name == "temporary-password" {
value = Some(Config::get_password()); value = Some(password::temporary_password());
} else if name == "permanent-password" {
value = Some(Config::get_permanent_password());
} else if name == "salt" { } else if name == "salt" {
value = Some(Config::get_salt()); value = Some(Config::get_salt());
} else if name == "rendezvous_server" { } 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" { } else if name == "rendezvous_servers" {
value = Some(Config::get_rendezvous_servers().join(",")); value = Some(Config::get_rendezvous_servers().join(","));
} else { } else {
@ -301,8 +359,10 @@ async fn handle(data: Data, stream: &mut Connection) {
if name == "id" { if name == "id" {
Config::set_key_confirmed(false); Config::set_key_confirmed(false);
Config::set_id(&value); Config::set_id(&value);
} else if name == "password" { } else if name == "temporary-password" {
Config::set_password(&value); password::update_temporary_password();
} else if name == "permanent-password" {
Config::set_permanent_password(&value);
} else if name == "salt" { } else if name == "salt" {
Config::set_salt(&value); Config::set_salt(&value);
} else { } else {
@ -339,6 +399,9 @@ async fn handle(data: Data, stream: &mut Connection) {
.await .await
); );
} }
Data::TestRendezvousServer => {
crate::test_rendezvous_server();
}
_ => {} _ => {}
} }
} }
@ -450,8 +513,8 @@ where
} }
} }
pub async fn send_raw(&mut self, data: Vec<u8>) -> ResultType<()> { pub async fn send_raw(&mut self, data: Bytes) -> ResultType<()> {
self.inner.send(bytes::Bytes::from(data)).await?; self.inner.send(data).await?;
Ok(()) Ok(())
} }
@ -492,9 +555,22 @@ pub async fn set_config(name: &str, value: String) -> ResultType<()> {
set_config_async(name, value).await set_config_async(name, value).await
} }
pub fn set_password(v: String) -> ResultType<()> { pub fn update_temporary_password() -> ResultType<()> {
Config::set_password(&v); set_config("temporary-password", "".to_owned())
set_config("password", v) }
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 { pub fn get_id() -> String {
@ -513,20 +589,17 @@ pub fn get_id() -> String {
} }
} }
pub fn get_password() -> String { pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec<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 {
if let Ok(Some(v)) = get_config_async("rendezvous_server", ms_timeout).await { 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<String> = urls.map(|x| x.to_owned()).collect();
(a, b)
} else { } 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?; .await?;
Ok(()) 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(())
}

291
src/lan.rs Normal file
View File

@ -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<Vec<Ipv4Addr>> {
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<String> {
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<A: ToSocketAddrs>(peer: A) -> Option<IpAddr> {
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<Vec<UdpSocket>> {
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<Vec<UdpSocket>> {
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<std::time::Duration>,
tx: UnboundedSender<config::DiscoveryPeer>,
) -> 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<UdpSocket>) -> UnboundedReceiver<config::DiscoveryPeer> {
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<config::DiscoveryPeer>) -> 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(())
}

View File

@ -1,20 +1,48 @@
use serde_json::{json, value::Value};
use std::ops::Deref; use std::ops::Deref;
mod cn; mod cn;
mod cs; mod cs;
mod da; mod da;
mod sk;
mod de; mod de;
mod en; mod en;
mod es;
mod eo; mod eo;
mod es;
mod hu;
mod fr; mod fr;
mod id; mod id;
mod it; mod it;
mod ptbr; mod ptbr;
mod ru; mod ru;
mod sk;
mod tr; mod tr;
mod tw; 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")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn translate(name: String) -> String { pub fn translate(name: String) -> String {
@ -46,6 +74,7 @@ pub fn translate_locale(name: String, locale: &str) -> String {
"tw" => tw::T.deref(), "tw" => tw::T.deref(),
"de" => de::T.deref(), "de" => de::T.deref(),
"es" => es::T.deref(), "es" => es::T.deref(),
"hu" => hu::T.deref(),
"ru" => ru::T.deref(), "ru" => ru::T.deref(),
"eo" => eo::T.deref(), "eo" => eo::T.deref(),
"id" => id::T.deref(), "id" => id::T.deref(),
@ -56,6 +85,8 @@ pub fn translate_locale(name: String, locale: &str) -> String {
"cs" => cs::T.deref(), "cs" => cs::T.deref(),
"da" => da::T.deref(), "da" => da::T.deref(),
"sk" => sk::T.deref(), "sk" => sk::T.deref(),
"vn" => vn::T.deref(),
"pl" => pl::T.deref(),
_ => en::T.deref(), _ => en::T.deref(),
}; };
if let Some(v) = m.get(&name as &str) { if let Some(v) = m.get(&name as &str) {

View File

@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "关于"), ("About", "关于"),
("Mute", "静音"), ("Mute", "静音"),
("Audio Input", "音频输入"), ("Audio Input", "音频输入"),
("Enhancements", "增强功能"),
("Hardware Codec", "硬件编解码"),
("Adaptive Bitrate", "自适应码率"),
("ID Server", "ID服务器"), ("ID Server", "ID服务器"),
("Relay Server", "中继服务器"), ("Relay Server", "中继服务器"),
("API Server", "API服务器"), ("API Server", "API服务器"),
@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "优化反应时间"), ("Optimize reaction time", "优化反应时间"),
("Custom", "自定义画质"), ("Custom", "自定义画质"),
("Show remote cursor", "显示远程光标"), ("Show remote cursor", "显示远程光标"),
("Show quality monitor", "显示质量监测"),
("Disable clipboard", "禁止剪贴板"), ("Disable clipboard", "禁止剪贴板"),
("Lock after session end", "断开后锁定远程电脑"), ("Lock after session end", "断开后锁定远程电脑"),
("Insert", "插入"), ("Insert", "插入"),
@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "退出"), ("Turned off", "退出"),
("In privacy mode", "进入隐私模式"), ("In privacy mode", "进入隐私模式"),
("Out 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(); ].iter().cloned().collect();
} }

View File

@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "O aplikaci"), ("About", "O aplikaci"),
("Mute", "Ztlumit"), ("Mute", "Ztlumit"),
("Audio Input", "Vstup zvuku"), ("Audio Input", "Vstup zvuku"),
("Enhancements", ""),
("Hardware Codec", ""),
("Adaptive Bitrate", ""),
("ID Server", "Server pro identif."), ("ID Server", "Server pro identif."),
("Relay Server", "Předávací (relay) server"), ("Relay Server", "Předávací (relay) server"),
("API Server", "Server s API rozhraním"), ("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"), ("Optimize reaction time", "Optimalizovat pro co nejnižší prodlevu odezvy"),
("Custom", "Uživatelsky určené"), ("Custom", "Uživatelsky určené"),
("Show remote cursor", "Zobrazovat ukazatel myši z protějšku"), ("Show remote cursor", "Zobrazovat ukazatel myši z protějšku"),
("Show quality monitor", ""),
("Disable clipboard", "Vypnout schránku"), ("Disable clipboard", "Vypnout schránku"),
("Lock after session end", "Po ukončení relace zamknout plochu"), ("Lock after session end", "Po ukončení relace zamknout plochu"),
("Insert", "Vložit"), ("Insert", "Vložit"),
@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Vypnutý"), ("Turned off", "Vypnutý"),
("In privacy mode", "v režimu soukromí"), ("In privacy mode", "v režimu soukromí"),
("Out privacy mode", "mimo režim 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(); ].iter().cloned().collect();
} }

View File

@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "Omkring"), ("About", "Omkring"),
("Mute", "Sluk for mikrofonen"), ("Mute", "Sluk for mikrofonen"),
("Audio Input", "Lydindgang"), ("Audio Input", "Lydindgang"),
("Enhancements", ""),
("Hardware Codec", ""),
("Adaptive Bitrate", ""),
("ID Server", "identifikations Server"), ("ID Server", "identifikations Server"),
("Relay Server", "Relæ Server"), ("Relay Server", "Relæ Server"),
("API Server", "API 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"), ("Optimize reaction time", "Optimeret responstid"),
("Custom", "Brugerdefineret"), ("Custom", "Brugerdefineret"),
("Show remote cursor", "Vis fjernbetjeningskontrolleret markør"), ("Show remote cursor", "Vis fjernbetjeningskontrolleret markør"),
("Show quality monitor", ""),
("Disable clipboard", "Deaktiver udklipsholder"), ("Disable clipboard", "Deaktiver udklipsholder"),
("Lock after session end", "Lås efter afslutningen af fjernstyring"), ("Lock after session end", "Lås efter afslutningen af fjernstyring"),
("Insert", "Indsæt"), ("Insert", "Indsæt"),
@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Slukket"), ("Turned off", "Slukket"),
("In privacy mode", "I databeskyttelsestilstand"), ("In privacy mode", "I databeskyttelsestilstand"),
("Out privacy mode", "Databeskyttelsestilstand fra"), ("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(); ].iter().cloned().collect();
} }

View File

@ -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."), ("desk_tip", "Mit dieser ID und diesem Passwort können Sie auf Ihren Desktop zugreifen."),
("Password", "Passwort"), ("Password", "Passwort"),
("Ready", "Bereit"), ("Ready", "Bereit"),
("Established", "Etabliert"), ("Established", "Verbunden"),
("connecting_status", "Verbinden mit dem RustDesk-Netzwerk..."), ("connecting_status", "Verbinden mit dem RustDesk-Netzwerk..."),
("Enable Service", "Verbindungsserver einschalten"), ("Enable Service", "Verbindungsserver aktivieren"),
("Start Service", "Starte Verbindungsserver"), ("Start Service", "Starte Vermittlungsdienst"),
("Service is running", "Dienst läuft"), ("Service is running", "Vermittlungsdienst aktiv"),
("Service is not running", "Der Verbindungsserver läuft nicht"), ("Service is not running", "Vermittlungsdienst deaktiviert"),
("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Verbindung"), ("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"), ("Transfer File", "Datei übertragen"),
("Connect", "Verbinden"), ("Connect", "Verbinden"),
("Recent Sessions", "Letzte Sitzungen"), ("Recent Sessions", "Letzte Sitzungen"),
@ -21,46 +21,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Confirmation", "Bestätigung"), ("Confirmation", "Bestätigung"),
("TCP Tunneling", "TCP Tunneln"), ("TCP Tunneling", "TCP Tunneln"),
("Remove", "Entfernen"), ("Remove", "Entfernen"),
("Refresh random password", "Zufälliges Passwort aktualisieren"), ("Refresh random password", "Neues zufälliges Passwort"),
("Set your own password", "Legen Sie Ihr eigenes Passwort fest"), ("Set your own password", "Eigenes Passwort setzen"),
("Enable Keyboard/Mouse", "Tastatur/Maus einschalten"), ("Enable Keyboard/Mouse", "Tastatur/Maus aktivieren"),
("Enable Clipboard", "Zwischenablage einschalten"), ("Enable Clipboard", "Zwischenablage aktivieren"),
("Enable File Transfer", "Dateiübertragung aktivieren"), ("Enable File Transfer", "Dateiübertragung aktivieren"),
("Enable TCP Tunneling", "TCP-Tunneling einschalten"), ("Enable TCP Tunneling", "TCP-Tunneln aktivieren"),
("IP Whitelisting", "IP Freigabeliste"), ("IP Whitelisting", "IP-Whitelist"),
("ID/Relay Server", "ID/Verbindungsserver"), ("ID/Relay Server", "ID/Vermittlungsserver"),
("Stop service", "Verbindungsserver ausschalten"), ("Stop service", "Vermittlungsdienst deaktivieren"),
("Change ID", "ID wechseln"), ("Change ID", "ID ändern"),
("Website", "Webseite"), ("Website", "Webseite"),
("About", "Über"), ("About", "Über"),
("Mute", "Stummschalten"), ("Mute", "Stummschalten"),
("Audio Input", "Audio-Eingang"), ("Audio Input", "Audio-Eingang"),
("Enhancements", "Verbesserungen"),
("Hardware Codec", "Hardware-Codec"),
("Adaptive Bitrate", "Adaptive Bitrate"),
("ID Server", "ID Server"), ("ID Server", "ID Server"),
("Relay Server", "Verbindungsserver Server"), ("Relay Server", "Vermittlungsserver"),
("API Server", "API Server"), ("API Server", "API-Server"),
("invalid_http", "Muss mit http:// oder https:// beginnen"), ("invalid_http", "Muss mit http:// oder https:// beginnen"),
("Invalid IP", "Ungültige IP-Adresse"), ("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"), ("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"), ("Not available", "Nicht verfügbar"),
("Too frequent", "Zu häufig"), ("Too frequent", "Zu häufig"),
("Cancel", "Abbrechen"), ("Cancel", "Abbrechen"),
("Skip", "Überspringen"), ("Skip", "Überspringen"),
("Close", "Schließen"), ("Close", "Schließen"),
("Retry", "Nochmal versuchen"), ("Retry", "Erneut versuchen"),
("OK", "OK"), ("OK", "OK"),
("Password Required", "Passwort erforderlich"), ("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"), ("Remember password", "Passwort merken"),
("Wrong Password", "Falsches Passwort"), ("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"), ("Connection Error", "Verbindungsfehler"),
("Error", "Fehler"), ("Error", "Fehler"),
("Reset by the peer", "Zurücksetzen durch die Gegenstelle"), ("Reset by the peer", "Verbindung wurde von der Gegenstelle zurückgesetzt"),
("Connecting...", "Verbinden..."), ("Connecting...", "Verbinden..."),
("Connection in progress. Please wait.", "Die Verbindung wird hergestellt. Bitte warten Sie."), ("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"), ("Login Error", "Anmeldefehler"),
("Successful", "Erfolgreich"), ("Successful", "Erfolgreich"),
("Connected, waiting for image...", "Verbunden, warten auf Bild..."), ("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"), ("Receive", "Empfangen"),
("Send", "Senden"), ("Send", "Senden"),
("Refresh File", "Datei aktualisieren"), ("Refresh File", "Datei aktualisieren"),
("Local", "Lokaler"), ("Local", "Lokal"),
("Remote", "Entfernter"), ("Remote", "Entfernt"),
("Remote Computer", "Entfernter Computer"), ("Remote Computer", "Entfernter Computer"),
("Local Computer", "Lokaler Computer"), ("Local Computer", "Dieser Computer"),
("Confirm Delete", "Löschen bestätigen"), ("Confirm Delete", "Löschen bestätigen"),
("Delete", "Löschen"), ("Delete", "Löschen"),
("Properties", "Eigenschaften"), ("Properties", "Eigenschaften"),
("Multi Select", "Mehrfachauswahl"), ("Multi Select", "Mehrfachauswahl"),
("Empty Directory", "Leeres Verzeichnis"), ("Empty Directory", "Leerer Ordner"),
("Not an empty directory", "Kein leeres Verzeichnis"), ("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 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 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 Verzeichnisses 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", "Dies gilt für alle Konflikte"), ("Do this for all conflicts", "Für alle Konflikte merken"),
("This is irreversible!", "Dies ist irreversibel!"), ("This is irreversible!", "Dies ist irreversibel!"),
("Deleting", "Löschen"), ("Deleting", "Löschen"),
("files", "Dateien"), ("files", "Dateien"),
("Waiting", "Warten"), ("Waiting", "Warten"),
("Finished", "Fertiggestellt"), ("Finished", "Fertiggestellt"),
("Speed", "Geschwindigkeit"), ("Speed", "Geschwindigkeit"),
("Custom Image Quality", "Individuelle Bildqualität"), ("Custom Image Quality", "Benutzerdefinierte Bildqualität"),
("Privacy mode", "Datenschutz-Modus"), ("Privacy mode", "Datenschutz-Modus"),
("Block user input", "Benutzereingaben blockieren"), ("Block user input", "Benutzereingaben blockieren"),
("Unblock user input", "Benutzereingaben freigeben"), ("Unblock user input", "Benutzereingaben freigeben"),
("Adjust Window", "Fenster anpassen"), ("Adjust Window", "Fenster anpassen"),
("Original", "Original"), ("Original", "Original"),
("Shrink", "Geschrumpft"), ("Shrink", "Verkleinern"),
("Stretch", "Gestreckt"), ("Stretch", "Strecken"),
("Good image quality", "Gute Bildqualität"), ("Good image quality", "Schöner"),
("Balanced", "Ausgeglichen"), ("Balanced", "Ausgeglichen"),
("Optimize reaction time", "Optimierte Reaktionszeit"), ("Optimize reaction time", "Schneller"),
("Custom", "Benutzerdefiniert"), ("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"), ("Disable clipboard", "Zwischenablage deaktivieren"),
("Lock after session end", "Sperren nach Sitzungsende"), ("Lock after session end", "Sperren nach Sitzungsende"),
("Insert", "Einfügen"), ("Insert", "Einfügen"),
("Insert Lock", "Sperre einfügen"), ("Insert Lock", "Win+L (Sperren) senden"),
("Refresh", "Aktualisieren"), ("Refresh", "Aktualisieren"),
("ID does not exist", "Die ID existiert nicht"), ("ID does not exist", "Diese ID existiert nicht"),
("Failed to connect to rendezvous server", "Verbindung zum Verbindungsserver fehlgeschlagen"), ("Failed to connect to rendezvous server", "Verbindung zum Vermittlungsserver fehlgeschlagen"),
("Please try later", "Bitte versuchen Sie es später"), ("Please try later", "Bitte versuchen Sie es später erneut"),
("Remote desktop is offline", "Entfernter Desktop ist offline"), ("Remote desktop is offline", "Entfernter PC ist offline"),
("Key mismatch", "Schlüssel nicht übereinstimmend"), ("Key mismatch", "Schlüssel stimmt nicht mit Serverschlüssel überein"),
("Timeout", "Zeitüberschreitung"), ("Timeout", "Zeitüberschreitung"),
("Failed to connect to relay server", "Verbindung zum Verbindungsserver fehlgeschlagen"), ("Failed to connect to relay server", "Verbindung zum Vermittlungsserver fehlgeschlagen"),
("Failed to connect via rendezvous server", "Verbindung über rendezvous server fehlgeschlagen"), ("Failed to connect via rendezvous server", "Verbindung über Vermittlungsserver ist fehlgeschlagen"),
("Failed to connect via relay server", "Verbindung über den Verbindungsserver 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-Desktop konnte nicht hergestellt werden"), ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten PC fehlgeschlagen"),
("Set Password", "Passwort festlegen"), ("Set Password", "Passwort festlegen"),
("OS Password", "Betriebssystem-Passwort"), ("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"), ("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 download", "Zum Herunterladen klicken"),
("Click to update", "Zum Aktualisieren klicken"), ("Click to update", "Zum Aktualisieren klicken"),
("Configure", "Konfigurieren"), ("Configure", "Konfigurieren"),
("config_acc", "Um Ihren Desktop aus der Ferne zu steuern, müssen Sie RustDesk \"Zugangs\" Rechte 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 Desktop zugreifen zu können, müssen Sie RustDesk \"Bildschirm-Aufnahme\" Berechtigungen erteilen."), ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk \"Bildschirm-Aufnahme\"-Berechtigung erteilen."),
("Installing ...", "Installiere ..."), ("Installing ...", "Installiere ..."),
("Install", "Installieren"), ("Install", "Installieren"),
("Installation", "Einrichtung"), ("Installation", "Installation"),
("Installation Path", "Einrichtungs Pfad"), ("Installation Path", "Installationspfad"),
("Create start menu shortcuts", "Startmenü Verknüpfungen erstellen"), ("Create start menu shortcuts", "Verknüpfung im Startmenü erstellen"),
("Create desktop icon", "Desktop Symbol erstellen"), ("Create desktop icon", "Desktop-Verknüpfung erstellen"),
("agreement_tip", "Wenn Sie die Einrichtung starten, akzeptieren Sie die Lizenzvereinbarung"), ("agreement_tip", "Durch die Installation akzeptieren Sie die Lizenzvereinbarung"),
("Accept and Install", "Akzeptieren und installieren"), ("Accept and Install", "Akzeptieren und Installieren"),
("End-user license agreement", "Lizenzvereinbarung für Endbenutzer"), ("End-user license agreement", "Lizenzvereinbarung für Endbenutzer"),
("Generating ...", "Generierung ..."), ("Generating ...", "Generiere..."),
("Your installation is lower version.", "Ihre Installation ist eine niedrigere Version."), ("Your installation is lower version.", "Ihre Installation ist älter."),
("not_close_tcp_tip", "Schließen Sie dieses Fenster nicht, während Sie den Tunnel benutzen."), ("not_close_tcp_tip", "Schließen Sie dieses Fenster nicht, solange Sie den Tunnel benutzen."),
("Listening ...", "Hören ..."), ("Listening ...", "Höre..."),
("Remote Host", "Entfernter Rechner"), ("Remote Host", "Entfernter PC"),
("Remote Port", "Entfernter Port"), ("Remote Port", "Entfernter Port"),
("Action", "Aktion"), ("Action", "Aktion"),
("Add", "Hinzufügen"), ("Add", "Hinzufügen"),
("Local Port", "Lokaler Port"), ("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."), ("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"), ("Permissions", "Berechtigungen"),
("Accept", "Akzeptieren"), ("Accept", "Akzeptieren"),
("Dismiss", "Ablehnen"), ("Dismiss", "Ablehnen"),
("Disconnect", "Verbindung trennen"), ("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 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"), ("Allow file copy and paste", "Kopieren und Einfügen von Dateien zulassen"),
("Connected", "Verbunden"), ("Connected", "Verbunden"),
("Direct and encrypted connection", "Direkte und verschlüsselte Verbindung"), ("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"), ("Enter your password", "Geben Sie Ihr Passwort ein"),
("Logging in...", "Anmeldung..."), ("Logging in...", "Anmeldung..."),
("Enable RDP session sharing", "RDP-Sitzungsfreigabe aktivieren"), ("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"), ("Enable Direct IP Access", "Direkten IP-Zugang aktivieren"),
("Rename", "Umbenennen"), ("Rename", "Umbenennen"),
("Space", "Platz"), ("Space", "Speicherplatz"),
("Create Desktop Shortcut", "Desktop-Verknüpfung erstellen"), ("Create Desktop Shortcut", "Desktop-Verknüpfung erstellen"),
("Change Path", "Pfad ändern"), ("Change Path", "Pfad ändern"),
("Create Folder", "Ordner erstellen"), ("Create Folder", "Ordner erstellen"),
("Please enter the folder name", "Bitte geben Sie den Ordnernamen ein"), ("Please enter the folder name", "Bitte geben Sie den Ordnernamen ein"),
("Fix it", "Reparieren"), ("Fix it", "Reparieren"),
("Warning", "Warnung"), ("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"), ("Reboot required", "Neustart erforderlich"),
("Unsupported display server ", "Nicht unterstützter Display-Server"), ("Unsupported display server ", "Nicht unterstützter Display-Server"),
("x11 expected", "X11 erwartet"), ("x11 expected", "X11 erwartet"),
@ -185,14 +189,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Settings", "Einstellungen"), ("Settings", "Einstellungen"),
("Username", " Benutzername"), ("Username", " Benutzername"),
("Invalid port", "Ungültiger Port"), ("Invalid port", "Ungültiger Port"),
("Closed manually by the peer", "Vom Peer manuell geschlossen"), ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"),
("Enable remote configuration modification", "Änderung der Fernkonfiguration zulassen"), ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"),
("Run without install", "Ohne Installation ausführen"), ("Run without install", "Ohne Installation ausführen"),
("Always connected via relay", "Immer über Verbindungsserver verbunden"), ("Always connected via relay", "Immer über Relay-Server verbunden"),
("Always connect via relay", "Verbindung immer über Verbindungsserver"), ("Always connect via relay", "Immer über Relay-Server verbinden"),
("whitelist_tip", "Nur IPs auf der Freigabeliste können auf mich zugreifen"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen"),
("Login", "Anmeldung"), ("Login", "Anmelden"),
("Logout", "Abmeldung"), ("Logout", "Abmelden"),
("Tags", "Stichworte"), ("Tags", "Stichworte"),
("Search ID", "Suche ID"), ("Search ID", "Suche ID"),
("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt"), ("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"), ("Add Tag", "Stichwort hinzufügen"),
("Unselect all tags", "Alle Stichworte abwählen"), ("Unselect all tags", "Alle Stichworte abwählen"),
("Network error", "Netzwerkfehler"), ("Network error", "Netzwerkfehler"),
("Username missed", "Benutzername fehlt"), ("Username missed", "Benutzername vergessen"),
("Password missed", "Passwort vergessen"), ("Password missed", "Passwort vergessen"),
("Wrong credentials", "Falsche Anmeldedaten"), ("Wrong credentials", "Falsche Anmeldedaten"),
("Edit Tag", "Stichwort bearbeiten"), ("Edit Tag", "Stichwort bearbeiten"),
("Unremember Password", "Passwort nicht merken"), ("Unremember Password", "Passwort vergessen"),
("Favorites", "Favoriten"), ("Favorites", "Favoriten"),
("Add to Favorites", "Zu Favoriten hinzufügen"), ("Add to Favorites", "Zu Favoriten hinzufügen"),
("Remove from Favorites", "Entferne von Favoriten"), ("Remove from Favorites", "Aus Favoriten entfernen"),
("Empty", "Leer"), ("Empty", "Leer"),
("Invalid folder name", "Ungültiger Ordnername"), ("Invalid folder name", "Ungültiger Ordnername"),
("Socks5 Proxy", "Socks5 Proxy"), ("Socks5 Proxy", "Socks5 Proxy"),
("Hostname", "Rechnername"), ("Hostname", "Rechnername"),
("Discovered", "Gefunden"), ("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"), ("Remote ID", "Entfernte ID"),
("Paste", "Einfügen"), ("Paste", "Einfügen"),
("Paste here?", "Hier 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"), ("Download new version", "Neue Version herunterladen"),
("Touch mode", "Touch-Modus"), ("Touch mode", "Touch-Modus"),
("Mouse mode", "Mouse-Modus"), ("Mouse mode", "Maus-Modus"),
("One-Finger Tap", "Ein Fingertipp"), ("One-Finger Tap", "1-Finger-Tipp"),
("Left Mouse", "Linke Maus"), ("Left Mouse", "Linksklick"),
("One-Long Tap", "Tippen Sie mit einem Finger lang"), ("One-Long Tap", "1-Finger-Halten"),
("Two-Finger Tap", "Zwei Finger tippen"), ("Two-Finger Tap", "2-Finger-Tipp"),
("Right Mouse", "Rechte Maus"), ("Right Mouse", "Rechtsklick"),
("One-Finger Move", "Eine Fingerbewegung"), ("One-Finger Move", "Einen Finger bewegen"),
("Double Tap & Move", "Doppeltippen und verschieben"), ("Double Tap & Move", "Doppeltippen und bewegen"),
("Mouse Drag", "Maus ziehen"), ("Mouse Drag", "Maus bewegen"),
("Three-Finger vertically", "Drei Finger vertikal"), ("Three-Finger vertically", "Drei Finger vertikal bewegen"),
("Mouse Wheel", "Mausrad"), ("Mouse Wheel", "Mausrad"),
("Two-Finger Move", "Zwei Finger Bewegung"), ("Two-Finger Move", "Zwei Finger bewegen"),
("Canvas Move", "Leinwand bewegen"), ("Canvas Move", "Sichtfeld bewegen"),
("Pinch to Zoom", "Zum Zoomen kneifen"), ("Pinch to Zoom", "2-Finger-Zoom"),
("Canvas Zoom", "Leinwand Zoom"), ("Canvas Zoom", "Sichtfeld-Zoom"),
("Reset canvas", "Anzeige zurücksetzen"), ("Reset canvas", "Sichtfeld zurücksetzen"),
("No permission of file transfer", "Keine Erlaubnis zur Dateiübertragung"), ("No permission of file transfer", "Keine Dateizugriff-Berechtigung"),
("Note", "Notiz"), ("Note", "Anmerkung"),
("Connection", "Verbindung"), ("Connection", "Verbindung"),
("Share Screen", "Bildschirm freigeben"), ("Share Screen", "Bildschirm freigeben"),
("CLOSE", "NAH DRAN"), ("CLOSE", "DEAKTIV."),
("OPEN", "OFFEN"), ("OPEN", "AKTIVIER."),
("Chat", "Plaudern"), ("Chat", "Chat"),
("Total", "Gesamt"), ("Total", "Gesamt"),
("items", "Artikel"), ("items", "Einträge"),
("Selected", "Ausgewählt"), ("Selected", "Ausgewählt"),
("Screen Capture", "Bildschirmaufnahme"), ("Screen Capture", "Bildschirmzugr."),
("Input Control", "Eingabesteuerung"), ("Input Control", "Eingabezugriff"),
("Audio Capture", "Audioaufnahme"), ("Audio Capture", "Audiozugriff"),
("File Connection", "Dateiverbindung"), ("File Connection", "Dateizugriff"),
("Screen Connection", "Bildschirmanschluss"), ("Screen Connection", "Bildschirmanschluss"),
("Do you accept?", "Akzeptieren Sie?"), ("Do you accept?", "Verbindung zulassen?"),
("Open System Setting", "Systemeinstellung öffnen"), ("Open System Setting", "Systemeinstellung öffnen"),
("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"), ("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_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_new_connection_tip", "möchte ihr Gerät steuern."),
("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_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 Schließen des Dienstes werden automatisch alle hergestellten Verbindungen geschlossen."), ("android_stop_service_tip", "Durch das Deaktivieren des Dienstes werden automatisch alle hergestellten Verbindungen getrennt."),
("android_version_audio_tip", "Die aktuelle Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher."), ("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 starten] oder ÖFFNEN Sie die Berechtigung [Bildschirmaufnahme], um den Bildschirmfreigabedienst zu starten."), ("android_start_service_tip", "Tippen Sie auf [Dienst aktivieren] oder aktivieren Sie die Berechtigung [Bildschirmzugr.], um den Bildschirmfreigabedienst zu starten."),
("Account", "Konto"), ("Account", "Konto"),
("Overwrite", "Überschreiben"), ("Overwrite", "Überschreiben"),
("This file exists, skip or overwrite this file?", "Diese Datei existiert, diese Datei überspringen oder überschreiben?"), ("This file exists, skip or overwrite this file?", "Diese Datei existiert; überspringen oder überschreiben?"),
("Quit", "Aufhören"), ("Quit", "Beenden"),
("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("doc_mac_permission", "https://rustdesk.com/docs/de/manual/mac/#berechtigungen-aktivieren"),
("Help", "Hilfe"), ("Help", "Hilfe"),
("Failed", "Gescheitert"), ("Failed", "Fehlgeschlagen"),
("Succeeded", "Erfolgreich"), ("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"), ("Unsupported", "Nicht unterstützt"),
("Peer denied", "Peer verweigert"), ("Peer denied", "Die Gegenstelle hat die Verbindung abgelehnt"),
("Please install plugins", "Bitte installieren Sie Plugins"), ("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"), ("Failed to turn off", "Ausschalten fehlgeschlagen"),
("Turned off", "Ausgeschaltet"), ("Turned off", "Ausgeschaltet"),
("In privacy mode", "im Datenschutzmodus"), ("In privacy mode", "Datenschutzmodus aktivieren"),
("Out privacy mode", "Datenschutzmodus aus"), ("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(); ].iter().cloned().collect();
} }

View File

@ -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_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"),
("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"),
("server_not_support", "Not yet supported by the server"), ("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(); ].iter().cloned().collect();
} }

View File

@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "Pri"), ("About", "Pri"),
("Mute", "Muta"), ("Mute", "Muta"),
("Audio Input", "Aŭdia enigo"), ("Audio Input", "Aŭdia enigo"),
("Enhancements", ""),
("Hardware Codec", ""),
("Adaptive Bitrate", ""),
("ID Server", "Servilo de identigiloj"), ("ID Server", "Servilo de identigiloj"),
("Relay Server", "Relajsa servilo"), ("Relay Server", "Relajsa servilo"),
("API Server", "Servilo de API"), ("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"), ("Optimize reaction time", "Optimigi reakcia tempo"),
("Custom", "Personigi bilda kvalito"), ("Custom", "Personigi bilda kvalito"),
("Show remote cursor", "Montri foran kursoron"), ("Show remote cursor", "Montri foran kursoron"),
("Show quality monitor", ""),
("Disable clipboard", "Malebligi poŝon"), ("Disable clipboard", "Malebligi poŝon"),
("Lock after session end", "Ŝlosi foran komputilon post malkonektado"), ("Lock after session end", "Ŝlosi foran komputilon post malkonektado"),
("Insert", "Enmeti"), ("Insert", "Enmeti"),
@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", ""), ("Turned off", ""),
("In privacy mode", ""), ("In privacy mode", ""),
("Out 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(); ].iter().cloned().collect();
} }

View File

@ -3,15 +3,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[ [
("Status", "Estado"), ("Status", "Estado"),
("Your Desktop", "Tu escritorio"), ("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"), ("Password", "Contraseña"),
("Ready", "Listo"), ("Ready", "Listo"),
("Established", "Establecido"), ("Established", "Establecido"),
("connecting_status", "Conexión a la red RustDesk en progreso..."), ("connecting_status", "Conexión a la red RustDesk en progreso..."),
("Enable Service", "Habilitar Servicio"), ("Enable Service", "Habilitar Servicio"),
("Start Service", "Iniciar Servicio"), ("Start Service", "Iniciar Servicio"),
("Service is running", "Servicio se está ejecutando"), ("Service is running", "El servicio se está ejecutando"),
("Service is not running", "Servicio no se está ejecutando"), ("Service is not running", "El servicio no se está ejecutando"),
("not_ready_status", "No está listo. Comprueba tu conexión"), ("not_ready_status", "No está listo. Comprueba tu conexión"),
("Control Remote Desktop", "Controlar Escritorio Remoto"), ("Control Remote Desktop", "Controlar Escritorio Remoto"),
("Transfer File", "Transferir archivo"), ("Transfer File", "Transferir archivo"),
@ -27,14 +27,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable Clipboard", "Habilitar portapapeles"), ("Enable Clipboard", "Habilitar portapapeles"),
("Enable File Transfer", "Habilitar transferencia de archivos"), ("Enable File Transfer", "Habilitar transferencia de archivos"),
("Enable TCP Tunneling", "Habilitar tunel TCP"), ("Enable TCP Tunneling", "Habilitar tunel TCP"),
("IP Whitelisting", "Lista blanca IP"), ("IP Whitelisting", "Lista blanca de IP"),
("ID/Relay Server", "Servidor de ID/Relay"), ("ID/Relay Server", "Servidor ID/Relay"),
("Stop service", "Parar servicio"), ("Stop service", "Parar servicio"),
("Change ID", "Cambiar identificación"), ("Change ID", "Cambiar ID"),
("Website", "Sitio web"), ("Website", "Sitio web"),
("About", "Sobre"), ("About", "Acerca de"),
("Mute", "Silencio"), ("Mute", "Silencio"),
("Audio Input", "Entrada de audio"), ("Audio Input", "Entrada de audio"),
("Enhancements", "mejoras"),
("Hardware Codec", ""),
("Adaptive Bitrate", ""),
("ID Server", "ID server"), ("ID Server", "ID server"),
("Relay Server", "Server relay"), ("Relay Server", "Server relay"),
("API Server", "Server API"), ("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."), ("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"), ("Invalid format", "Formato inválido"),
("server_not_support", "Aún no es compatible con el servidor"), ("server_not_support", "Aún no es compatible con el servidor"),
("Not available", "Indisponible"), ("Not available", "No disponible"),
("Too frequent", "Demasiado frecuente"), ("Too frequent", "Demasiado frecuente"),
("Cancel", "Cancelar"), ("Cancel", "Cancelar"),
("Skip", "Saltar"), ("Skip", "Saltar"),
("Close", "Cerrar"), ("Close", "Cerrar"),
("Retry", "Volver"), ("Retry", "Reintentar"),
("OK", "OK"), ("OK", "OK"),
("Password Required", "Se requiere contraseña"), ("Password Required", "Se requiere contraseña"),
("Please enter your password", "Por favor, introduzca su 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"), ("Optimize reaction time", "Optimizar el tiempo de reacción"),
("Custom", "Personalizado"), ("Custom", "Personalizado"),
("Show remote cursor", "Mostrar cursor remoto"), ("Show remote cursor", "Mostrar cursor remoto"),
("Show quality monitor", "Mostrar calidad del monitor"),
("Disable clipboard", "Deshabilitar portapapeles"), ("Disable clipboard", "Deshabilitar portapapeles"),
("Lock after session end", "Bloquear después del final de la sesión"), ("Lock after session end", "Bloquear después del final de la sesión"),
("Insert", "Insertar"), ("Insert", "Insertar"),
@ -113,7 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ID does not exist", "ID no existe"), ("ID does not exist", "ID no existe"),
("Failed to connect to rendezvous server", "No se pudo conectar al servidor de encuentro"), ("Failed to connect to rendezvous server", "No se pudo conectar al servidor de encuentro"),
("Please try later", "Por favor intente mas tarde"), ("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"), ("Key mismatch", "La clave no coincide"),
("Timeout", "Timeout"), ("Timeout", "Timeout"),
("Failed to connect to relay server", "No se pudo conectar al servidor de retransmisión"), ("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."), ("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 upgrade", "Clic para actualizar"),
("Click to download", "Clic para descargar"), ("Click to download", "Clic para descargar"),
("Click to update", "Fare clic per aggiornare"), ("Click to update", "Clic para refrescar"),
("Configure", "Configurar"), ("Configure", "Configurar"),
("config_acc", "Para controlar su escritorio desde el exterior, debe otorgar permiso a RustDesk de \"Accesibilidad\"."), ("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\"."), ("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"), ("Turned off", "Apagado"),
("In privacy mode", "En modo de privacidad"), ("In privacy mode", "En modo de privacidad"),
("Out privacy mode", "Fuera del 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(); ].iter().cloned().collect();
} }

View File

@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "À propos de"), ("About", "À propos de"),
("Mute", "Muet"), ("Mute", "Muet"),
("Audio Input", "Entrée audio"), ("Audio Input", "Entrée audio"),
("Enhancements", ""),
("Hardware Codec", ""),
("Adaptive Bitrate", ""),
("ID Server", "Serveur ID"), ("ID Server", "Serveur ID"),
("Relay Server", "Serveur relais"), ("Relay Server", "Serveur relais"),
("API Server", "Serveur API"), ("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"), ("Optimize reaction time", "Optimiser le temps de réaction"),
("Custom", "Qualité d'image personnalisée"), ("Custom", "Qualité d'image personnalisée"),
("Show remote cursor", "Afficher le curseur distant"), ("Show remote cursor", "Afficher le curseur distant"),
("Show quality monitor", ""),
("Disable clipboard", "Désactiver le presse-papier"), ("Disable clipboard", "Désactiver le presse-papier"),
("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"), ("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"),
("Insert", "Insérer"), ("Insert", "Insérer"),
@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Éteindre"), ("Turned off", "Éteindre"),
("In privacy mode", "en mode privé"), ("In privacy mode", "en mode privé"),
("Out privacy mode", "hors mode de confidentialité"), ("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(); ].iter().cloned().collect();
} }

Some files were not shown because too many files have changed in this diff Show More