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
run: |
case ${{ matrix.job.target }} in
x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev;;
x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;;
# arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
# aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
esac

1
.gitignore vendored
View File

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

View File

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

8
DEBIAN/postinst Normal file → Executable file
View File

@ -8,16 +8,20 @@ if [ "$1" = configure ]; then
if [ "systemd" == "$INITSYS" ]; then
if [ -e /etc/systemd/system/rustdesk.service ]; then
rm /etc/systemd/system/rustdesk.service
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
fi
version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)')
parsedVersion=$(echo "${version//./}")
if [[ "$parsedVersion" -gt "360" ]]; then
sudo -H pip3 install pynput
fi
cp /usr/share/rustdesk/files/systemd/rustdesk.service /etc/systemd/system/rustdesk.service
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
systemctl daemon-reload
systemctl enable rustdesk
systemctl start rustdesk
cp /usr/share/rustdesk/files/systemd/rustdesk.service.user /usr/lib/systemd/user/rustdesk.service
curUser=$(who | awk '{print $1}' | head -1)
systemctl --machine=${curUser}@.host --user daemon-reload
fi
fi

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}')
if [ "systemd" == "${INITSYS}" ]; then
service rustdesk stop || true
serverUser=$(ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1)
if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ]
then
systemctl --machine=${serverUser}@.host --user stop rustdesk || true
fi
sleep 1
rm -rf /usr/bin/libsciter-gtk.so
fi

9
DEBIAN/prerm Normal file → Executable file
View File

@ -8,7 +8,14 @@ case $1 in
if [ "systemd" == "${INITSYS}" ]; then
systemctl stop rustdesk || true
systemctl disable rustdesk || true
rm /etc/systemd/system/rustdesk.service || true
serverUser=$(ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1)
if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ]
then
systemctl --machine=${serverUser}@.host --user stop rustdesk || true
fi
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service || true
fi
;;
esac

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a>
<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>
</p>

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Struktura</a>
<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>
</p>

View File

@ -5,11 +5,11 @@
<a href="#auf-docker-kompilieren">Docker</a>
<a href="#dateistruktur">Dateistruktur</a>
<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>
</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)

View File

@ -5,11 +5,11 @@
<a href="#kiel-kompili-kun-docker">Docker</a>
<a href="#dosierstrukturo">Strukturo</a>
<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>
</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)

View File

@ -5,7 +5,7 @@
<a href="#como-compilar-con-docker">Docker</a>
<a href="#estructura-de-archivos">Estructura</a>
<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>
</p>

View File

@ -5,11 +5,11 @@
<a dir="rtl" href="#نحوه-ساخت-با-داکر">داکر</a>
<a dir="rtl" href="#ساخت">ساخت</a>
<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> به زبان مادری شما به کمکتون نیاز داریم
</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)

View File

@ -5,11 +5,11 @@
<a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Rakenne</a>
<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>
</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)

View File

@ -5,11 +5,11 @@
<a href="#comment-construire-avec-docker">Docker</a> -
<a href="#structure-du-projet">Structure</a> -
<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>.
</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)

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="#file-structure">Structure</a>
<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>
</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)

View File

@ -5,11 +5,11 @@
<a href="#come-compilare-con-docker">Docker</a>
<a href="#struttura-dei-file">Struttura</a>
<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>
</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)

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a>
<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>
</p>

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a>
<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>
</p>

View File

@ -5,11 +5,11 @@
<a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a>
<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>
</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)

View File

@ -5,11 +5,11 @@
<a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structuur</a>
<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>
</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)

View File

@ -5,11 +5,11 @@
<a href="#jak-kompilować-za-pomocą-dockera">Docker</a>
<a href="#struktura-plików">Struktura</a>
<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>
</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)

View File

@ -5,11 +5,11 @@
<a href="#como-compilar-com-docker">Docker</a>
<a href="#estrutura-de-arquivos">Estrutura</a>
<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>
</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)

View File

@ -1,11 +1,11 @@
<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="#raw-steps-to-build">Build</a>
<a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a>
<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>
</p>
@ -15,10 +15,16 @@
Еще одно программное обеспечение для удаленного рабочего стола, написанное на Rust. Работает из коробки, не требует настройки. Вы полностью контролируете свои данные, не беспокоясь о безопасности. Вы можете использовать наш сервер ретрансляции, [настроить свой собственный](https://rustdesk.com/server), или [написать свой собственный сервер ретрансляции](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk приветствует вклад каждого. Смотрите [`CONTRIBUTING.md`](CONTRIBUTING.md) для помощи в начале работы.
[**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
[**СКАЧАТЬ ПРИЛОЖЕНИЕ**](https://github.com/rustdesk/rustdesk/releases)
[<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
```
### Исправление libvpx (Для Fedora)
### Исправление libvpx (для Fedora)
```sh
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="#文件结构">结构</a>
<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>
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
@ -202,7 +202,7 @@ target/release/rustdesk
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 截屏
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务audio/clipboard/input/video 服务, 已经连接实现
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务audio/clipboard/input/video 服务, 以及连接的实现
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持 UDP 通讯, 等待远程连接(通过打洞直连或者中继)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a>
<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>
</p>
@ -155,7 +155,7 @@ Or, if you're running a release executable:
target/release/rustdesk
```
Please ensure that you are running these commands from the root of the RustDesk repository, otherwise the application may be unable to find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host.
Please ensure that you are running these commands from the root of the RustDesk repository, otherwise the application might not be able to find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host.
## File Structure

42
build.py Normal file → Executable file
View File

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

View File

@ -3,6 +3,7 @@
package="com.carriez.flutter_hbb">
<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.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
@ -43,7 +44,7 @@
<activity
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:hardwareAccelerated="true"
android:launchMode="singleTop"

View File

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

View File

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

View File

@ -2,20 +2,26 @@ package com.carriez.flutter_hbb
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.media.AudioRecord
import android.media.AudioRecord.READ_BLOCKING
import android.media.MediaCodecList
import android.media.MediaFormat
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.os.PowerManager
import android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import java.nio.ByteBuffer
import java.util.*
@SuppressLint("ConstantLocale")
val LOCAL_NAME = Locale.getDefault().toString()
val SCREEN_INFO = Info(0, 0, 1, 200)
@ -38,8 +44,31 @@ fun testVP9Support(): Boolean {
return res != null
}
@RequiresApi(Build.VERSION_CODES.M)
fun requestPermission(context: Context, type: String) {
val permission = when (type) {
"ignore_battery_optimizations" -> {
try {
context.startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:" + context.packageName)
})
} catch (e:Exception) {
e.printStackTrace()
}
return
}
"application_details_settings" -> {
try {
context.startActivity(Intent().apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
action = "android.settings.APPLICATION_DETAILS_SETTINGS"
data = Uri.parse("package:" + context.packageName)
})
} catch (e:Exception) {
e.printStackTrace()
}
return
}
"audio" -> {
Permission.RECORD_AUDIO
}
@ -52,7 +81,7 @@ fun requestPermission(context: Context, type: String) {
}
XXPermissions.with(context)
.permission(permission)
.request { permissions, all ->
.request { _, all ->
if (all) {
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
@ -64,8 +93,13 @@ fun requestPermission(context: Context, type: String) {
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun checkPermission(context: Context, type: String): Boolean {
val permission = when (type) {
"ignore_battery_optimizations" -> {
val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager
return pw.isIgnoringBatteryOptimizations(context.packageName)
}
"audio" -> {
Permission.RECORD_AUDIO
}

View File

@ -282,7 +282,12 @@ class PermissionManager {
static Timer? _timer;
static var _current = "";
static final permissions = ["audio", "file"];
static final permissions = [
"audio",
"file",
"ignore_battery_optimizations",
"application_details_settings"
];
static bool isWaitingFile() {
if (_completer != null) {
@ -301,6 +306,10 @@ class PermissionManager {
if (!permissions.contains(type))
return Future.error("Wrong permission!$type");
gFFI.invokeMethod("request_permission", type);
if (type == "ignore_battery_optimizations") {
return Future.value(false);
}
_current = type;
_completer = Completer<bool>();
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
/// for session specific usage
FFI ffi(String? tag) {

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
@ -26,13 +27,100 @@ class SettingsPage extends StatefulWidget implements PageShape {
_SettingsState createState() => _SettingsState();
}
class _SettingsState extends State<SettingsPage> {
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
static const url = 'https://rustdesk.com/';
final _hasIgnoreBattery = androidVersion >= 26;
var _ignoreBatteryOpt = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
if (_hasIgnoreBattery) {
updateIgnoreBatteryStatus();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
updateIgnoreBatteryStatus();
}
}
Future<bool> updateIgnoreBatteryStatus() async {
final res = await PermissionManager.check("ignore_battery_optimizations");
if (_ignoreBatteryOpt != res) {
setState(() {
_ignoreBatteryOpt = res;
});
return true;
} else {
return false;
}
}
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
final username = getUsername();
final enableAbr = gFFI.getByName("option", "enable-abr") != 'N';
final enhancementsTiles = [
SettingsTile.switchTile(
title: Text(translate('Adaptive Bitrate') + '(beta)'),
initialValue: enableAbr,
onToggle: (v) {
final msg = Map()
..["name"] = "enable-abr"
..["value"] = "";
if (!v) {
msg["value"] = "N";
}
gFFI.setByName("option", json.encode(msg));
setState(() {});
},
)
];
if (_hasIgnoreBattery) {
enhancementsTiles.insert(
0,
SettingsTile.switchTile(
initialValue: _ignoreBatteryOpt,
title: Text(translate('Keep RustDesk background service')),
description:
Text('* ${translate('Ignore Battery Optimizations')}'),
onToggle: (v) async {
if (v) {
PermissionManager.request("ignore_battery_optimizations");
} else {
final res = await DialogManager.show<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(
sections: [
SettingsSection(
@ -53,17 +141,17 @@ class _SettingsState extends State<SettingsPage> {
),
],
),
SettingsSection(
title: Text(translate("Settings")),
tiles: [
SettingsTile.navigation(
SettingsSection(title: Text(translate("Settings")), tiles: [
SettingsTile.navigation(
title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud),
onPressed: (context) {
showServerSettings();
},
),
],
})
]),
SettingsSection(
title: Text(translate("Enhancements")),
tiles: enhancementsTiles,
),
SettingsSection(
title: Text(translate("About")),

View File

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

View File

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

View File

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

View File

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

76
lang.py
View File

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

View File

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

View File

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

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 self::libc::{c_char, c_int, c_void, useconds_t};
use std::{borrow::Cow, ffi::CString, io::prelude::*, ptr, sync::mpsc};
use std::{borrow::Cow, ffi::CString, ptr};
const CURRENT_WINDOW: c_int = 0;
const DEFAULT_DELAY: u64 = 12000;
@ -60,34 +60,25 @@ fn mousebutton(button: MouseButton) -> c_int {
}
}
enum PyMsg {
Char(char),
Str(&'static str),
}
/// The main struct for handling the event emitting
pub struct Enigo {
pub(super) struct EnigoXdo {
xdo: Xdo,
delay: u64,
tx: mpsc::Sender<(PyMsg, bool)>,
}
// This is safe, we have a unique pointer.
// TODO: use Unique<c_char> once stable.
unsafe impl Send for Enigo {}
unsafe impl Send for EnigoXdo {}
impl Default for Enigo {
/// Create a new Enigo instance
impl Default for EnigoXdo {
/// Create a new EnigoXdo instance
fn default() -> Self {
let (tx, rx) = mpsc::channel();
start_pynput_service(rx);
Self {
xdo: unsafe { xdo_new(ptr::null()) },
delay: DEFAULT_DELAY,
tx,
}
}
}
impl Enigo {
impl EnigoXdo {
/// Get the delay per keypress.
/// Default value is 12000.
/// This is Linux-specific.
@ -99,101 +90,8 @@ impl Enigo {
pub fn set_delay(&mut self, delay: u64) {
self.delay = delay;
}
///
pub fn reset(&mut self) {
self.tx.send((PyMsg::Char('\0'), true)).ok();
}
#[inline]
fn send_pynput(&mut self, key: &Key, is_press: bool) -> bool {
if unsafe { PYNPUT_EXIT || !PYNPUT_REDAY } {
return false;
}
if let Key::Layout(c) = key {
return self.tx.send((PyMsg::Char(*c), is_press)).is_ok();
}
if let Key::Raw(_) = key {
return false;
}
#[allow(deprecated)]
let s = match key {
Key::Alt => "Alt_L",
Key::Backspace => "BackSpace",
Key::CapsLock => "Caps_Lock",
Key::Control => "Control_L",
Key::Delete => "Delete",
Key::DownArrow => "Down",
Key::End => "End",
Key::Escape => "Escape",
Key::F1 => "F1",
Key::F10 => "F10",
Key::F11 => "F11",
Key::F12 => "F12",
Key::F2 => "F2",
Key::F3 => "F3",
Key::F4 => "F4",
Key::F5 => "F5",
Key::F6 => "F6",
Key::F7 => "F7",
Key::F8 => "F8",
Key::F9 => "F9",
Key::Home => "Home",
Key::LeftArrow => "Left",
Key::Option => "Option",
Key::PageDown => "Page_Down",
Key::PageUp => "Page_Up",
Key::Return => "Return",
Key::RightArrow => "Right",
Key::Shift => "Shift_L",
Key::Space => "space",
Key::Tab => "Tab",
Key::UpArrow => "Up",
Key::Numpad0 => "0",
Key::Numpad1 => "1",
Key::Numpad2 => "2",
Key::Numpad3 => "3",
Key::Numpad4 => "4",
Key::Numpad5 => "5",
Key::Numpad6 => "6",
Key::Numpad7 => "7",
Key::Numpad8 => "8",
Key::Numpad9 => "9",
Key::Decimal => "KP_Decimal",
Key::Cancel => "Cancel",
Key::Clear => "Clear",
Key::Pause => "Pause",
Key::Kana => "Kana",
Key::Hangul => "Hangul",
Key::Hanja => "Hanja",
Key::Kanji => "Kanji",
Key::Select => "Select",
Key::Print => "Print",
Key::Execute => "Execute",
Key::Snapshot => "3270_PrintScreen",
Key::Insert => "Insert",
Key::Help => "Help",
Key::Separator => "KP_Separator",
Key::Scroll => "Scroll_Lock",
Key::NumLock => "Num_Lock",
Key::RWin => "Super_R",
Key::Apps => "Menu",
Key::Multiply => "KP_Multiply",
Key::Add => "KP_Add",
Key::Subtract => "KP_Subtract",
Key::Divide => "KP_Divide",
Key::Equals => "KP_Equal",
Key::NumpadEnter => "KP_Enter",
Key::RightShift => "Shift_R",
Key::RightControl => "Control_R",
Key::RightAlt => "Mode_switch",
Key::Command | Key::Super | Key::Windows | Key::Meta => "Super_L",
_ => {
return true;
}
};
return self.tx.send((PyMsg::Str(s), is_press)).is_ok();
}
}
impl Drop for Enigo {
impl Drop for EnigoXdo {
fn drop(&mut self) {
if self.xdo.is_null() {
return;
@ -203,7 +101,7 @@ impl Drop for Enigo {
}
}
}
impl MouseControllable for Enigo {
impl MouseControllable for EnigoXdo {
fn mouse_move_to(&mut self, x: i32, y: i32) {
if self.xdo.is_null() {
return;
@ -378,7 +276,7 @@ fn keysequence<'a>(key: Key) -> Cow<'a, str> {
_ => "",
})
}
impl KeyboardControllable for Enigo {
impl KeyboardControllable for EnigoXdo {
fn get_key_state(&mut self, key: Key) -> bool {
if self.xdo.is_null() {
return false;
@ -431,9 +329,6 @@ impl KeyboardControllable for Enigo {
if self.xdo.is_null() {
return Ok(());
}
if self.send_pynput(&key, true) {
return Ok(());
}
let string = CString::new(&*keysequence(key))?;
unsafe {
xdo_send_keysequence_window_down(
@ -449,9 +344,6 @@ impl KeyboardControllable for Enigo {
if self.xdo.is_null() {
return;
}
if self.send_pynput(&key, false) {
return;
}
if let Ok(string) = CString::new(&*keysequence(key)) {
unsafe {
xdo_send_keysequence_window_up(
@ -479,127 +371,3 @@ impl KeyboardControllable for Enigo {
}
}
}
static mut PYNPUT_EXIT: bool = false;
static mut PYNPUT_REDAY: bool = false;
static IPC_FILE: &'static str = "/tmp/RustDesk/pynput_service";
fn start_pynput_service(rx: mpsc::Receiver<(PyMsg, bool)>) {
let mut py = "./pynput_service.py".to_owned();
if !std::path::Path::new(&py).exists() {
py = "/usr/share/rustdesk/files/pynput_service.py".to_owned();
if !std::path::Path::new(&py).exists() {
py = "/usr/lib/rustdesk/pynput_service.py".to_owned();
if !std::path::Path::new(&py).exists() {
// enigo libs, not rustdesk root project, so skip using appimage features
py = std::env::var("APPDIR").unwrap_or("".to_string()) + "/usr/lib/rustdesk/pynput_service.py";
if !std::path::Path::new(&py).exists() {
log::error!("{} not exists", py);
}
}
}
}
log::info!("pynput service: {}", py);
std::thread::spawn(move || {
let username = std::env::var("PYNPUT_USERNAME").unwrap_or("".to_owned());
let userid = std::env::var("PYNPUT_USERID").unwrap_or("".to_owned());
let status = if username.is_empty() {
std::process::Command::new("python3")
.arg(&py)
.arg(IPC_FILE)
.status()
.map(|x| x.success())
} else {
let mut status = Ok(true);
for i in 0..100 {
if i % 10 == 0 {
log::info!("#{} try to start pynput server", i);
}
status = std::process::Command::new("sudo")
.args(vec![
"-E",
&format!("XDG_RUNTIME_DIR=/run/user/{}", userid) as &str,
"-u",
&username,
"python3",
&py,
IPC_FILE,
])
.status()
.map(|x| x.success());
match status {
Ok(true) => break,
_ => {}
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
status
};
log::info!(
"pynput server exit with username/id {}/{}: {:?}",
username,
userid,
status
);
unsafe {
PYNPUT_EXIT = true;
}
});
std::thread::spawn(move || {
for i in 0..300 {
std::thread::sleep(std::time::Duration::from_millis(100));
let mut conn = match std::os::unix::net::UnixStream::connect(IPC_FILE) {
Ok(conn) => conn,
Err(err) => {
if i % 15 == 0 {
log::warn!("Failed to connect to {}: {}", IPC_FILE, err);
}
continue;
}
};
if let Err(err) = conn.set_nonblocking(true) {
log::error!("Failed to set ipc nonblocking: {}", err);
return;
}
log::info!("Conntected to pynput server");
let d = std::time::Duration::from_millis(30);
unsafe {
PYNPUT_REDAY = true;
}
let mut buf = [0u8; 1024];
loop {
if unsafe { PYNPUT_EXIT } {
break;
}
match rx.recv_timeout(d) {
Ok((msg, is_press)) => {
let msg = match msg {
PyMsg::Char(chr) => {
format!("{}{}", if is_press { 'p' } else { 'r' }, chr)
}
PyMsg::Str(s) => format!("{}{}", if is_press { 'p' } else { 'r' }, s),
};
let n = msg.len();
buf[0] = n as _;
buf[1..(n + 1)].copy_from_slice(msg.as_bytes());
if let Err(err) = conn.write_all(&buf[..n + 1]) {
log::error!("Failed to write to ipc: {}", err);
break;
}
}
Err(err) => match err {
mpsc::RecvTimeoutError::Disconnected => {
log::error!("pynput sender disconnecte");
break;
}
_ => {}
},
}
}
unsafe {
PYNPUT_REDAY = false;
}
break;
}
});
}

View File

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

View File

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

View File

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

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::{
collections::HashMap,
fs,
@ -12,11 +7,26 @@ use std::{
time::SystemTime,
};
use anyhow::Result;
use directories_next::ProjectDirs;
use rand::Rng;
use serde_derive::{Deserialize, Serialize};
use sodiumoxide::crypto::sign;
use crate::{
log,
password_security::{
decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original,
encrypt_vec_or_original,
},
};
pub const RENDEZVOUS_TIMEOUT: u64 = 12_000;
pub const CONNECT_TIMEOUT: u64 = 18_000;
pub const REG_INTERVAL: i64 = 12_000;
pub const COMPRESS_LEVEL: i32 = 3;
const SERIAL: i32 = 3;
const PASSWORD_ENC_VERSION: &'static str = "00";
// 128x128
#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding
pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII=
@ -38,7 +48,9 @@ lazy_static::lazy_static! {
pub static ref ONLINE: Arc<Mutex<HashMap<String, i64>>> = 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()));
static ref KEY_PAIR: Arc<Mutex<Option<(Vec<u8>, Vec<u8>)>>> = Default::default();
}
// #[cfg(any(target_os = "android", target_os = "ios"))]
lazy_static::lazy_static! {
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-cn.rustdesk.com",
];
pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=";
pub const RENDEZVOUS_PORT: i32 = 21116;
pub const RELAY_PORT: i32 = 21117;
@ -72,7 +85,7 @@ pub struct Config {
#[serde(default)]
salt: String,
#[serde(default)]
pub key_pair: (Vec<u8>, Vec<u8>), // sk, pk
key_pair: (Vec<u8>, Vec<u8>), // sk, pk
#[serde(default)]
key_confirmed: bool,
#[serde(default)]
@ -107,7 +120,7 @@ pub struct Config2 {
pub options: HashMap<String, String>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
pub struct PeerConfig {
#[serde(default)]
pub password: Vec<u8>,
@ -139,6 +152,8 @@ pub struct PeerConfig {
pub disable_clipboard: bool,
#[serde(default)]
pub enable_file_transfer: bool,
#[serde(default)]
pub show_quality_monitor: bool,
// the other scalar value must before this
#[serde(default)]
@ -159,7 +174,7 @@ pub struct PeerInfoSerde {
pub platform: String,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
pub struct TransferSerde {
#[serde(default)]
pub write_jobs: Vec<String>,
@ -198,7 +213,17 @@ fn patch(path: PathBuf) -> PathBuf {
impl 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 {
@ -206,7 +231,12 @@ impl Config2 {
}
fn store(&self) {
Config::store_(self, "2");
let mut config = self.clone();
if let Some(mut socks) = config.socks {
socks.password = encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION);
config.socks = Some(socks);
}
Config::store_(&config, "2");
}
pub fn get() -> Config2 {
@ -237,6 +267,11 @@ pub fn load_path<T: serde::Serialize + serde::de::DeserializeOwned + Default + s
cfg
}
#[inline]
pub fn store_path<T: serde::Serialize>(path: PathBuf, cfg: T) -> crate::ResultType<()> {
Ok(confy::store_path(path, cfg)?)
}
impl Config {
fn load_<T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug>(
suffix: &str,
@ -252,17 +287,25 @@ impl Config {
fn store_<T: serde::Serialize>(config: &T, suffix: &str) {
let file = Self::file_(suffix);
if let Err(err) = confy::store_path(file, config) {
if let Err(err) = store_path(file, config) {
log::error!("Failed to store config: {}", err);
}
}
fn load() -> Config {
Config::load_::<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) {
Config::store_(self, "");
let mut config = self.clone();
config.password = encrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION);
Config::store_(&config, "");
}
pub fn file() -> PathBuf {
@ -274,6 +317,10 @@ impl Config {
Config::with_extension(Self::path(name))
}
pub fn is_empty(&self) -> bool {
self.id.is_empty() || self.key_pair.0.is_empty()
}
pub fn get_home() -> PathBuf {
#[cfg(any(target_os = "android", target_os = "ios"))]
return Self::path(APP_HOME_DIR.read().unwrap().as_str());
@ -495,9 +542,9 @@ impl Config {
}
}
pub fn get_auto_password() -> String {
pub fn get_auto_password(length: usize) -> String {
let mut rng = rand::thread_rng();
(0..6)
(0..length)
.map(|_| CHARS[rng.gen::<usize>() % CHARS.len()])
.collect()
}
@ -535,24 +582,26 @@ impl Config {
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>) {
// 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() {
let (pk, sk) = sign::gen_keypair();
config.key_pair = (sk.0.to_vec(), pk.0.into());
config.store();
let key_pair = (sk.0.to_vec(), pk.0.into());
config.key_pair = key_pair.clone();
std::thread::spawn(|| {
let mut config = CONFIG.write().unwrap();
config.key_pair = key_pair;
config.store();
});
}
config.key_pair.clone()
*lock = Some(config.key_pair.clone());
return config.key_pair;
}
pub fn get_id() -> String {
@ -618,7 +667,7 @@ impl Config {
log::info!("id updated from {} to {}", id, new_id);
}
pub fn set_password(password: &str) {
pub fn set_permanent_password(password: &str) {
let mut config = CONFIG.write().unwrap();
if password == config.password {
return;
@ -627,13 +676,8 @@ impl Config {
config.store();
}
pub fn get_password() -> String {
let mut password = CONFIG.read().unwrap().password.clone();
if password.is_empty() {
password = Config::get_auto_password();
Config::set_password(&password);
}
password
pub fn get_permanent_password() -> String {
CONFIG.read().unwrap().password.clone()
}
pub fn set_salt(salt: &str) {
@ -648,7 +692,7 @@ impl Config {
pub fn get_salt() -> String {
let mut salt = CONFIG.read().unwrap().salt.clone();
if salt.is_empty() {
salt = Config::get_auto_password();
salt = Config::get_auto_password(6);
Config::set_salt(&salt);
}
salt
@ -705,7 +749,28 @@ impl PeerConfig {
pub fn load(id: &str) -> PeerConfig {
let _ = CONFIG.read().unwrap(); // for lock
match confy::load_path(&Self::path(id)) {
Ok(config) => config,
Ok(config) => {
let mut config: PeerConfig = config;
let mut store = false;
let (password, _, store2) =
decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION);
config.password = password;
store = store || store2;
config.options.get_mut("rdp_password").map(|v| {
let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION);
*v = password;
store = store || store2;
});
config.options.get_mut("os-password").map(|v| {
let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION);
*v = password;
store = store || store2;
});
if store {
config.store(id);
}
config
}
Err(err) => {
log::error!("Failed to load config: {}", err);
Default::default()
@ -715,7 +780,17 @@ impl PeerConfig {
pub fn store(&self, id: &str) {
let _ = CONFIG.read().unwrap(); // for lock
if let Err(err) = confy::store_path(Self::path(id), self) {
let mut config = self.clone();
config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION);
config
.options
.get_mut("rdp_password")
.map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION));
config
.options
.get_mut("os-password")
.map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION));
if let Err(err) = store_path(Self::path(id), config) {
log::error!("Failed to store config: {}", err);
}
}
@ -847,10 +922,26 @@ impl LocalConfig {
}
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct DiscoveryPeer {
pub id: String,
#[serde(with = "serde_with::rust::map_as_tuple_list")]
pub ip_mac: HashMap<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)]
pub struct LanPeers {
#[serde(default)]
pub peers: String,
pub peers: Vec<DiscoveryPeer>,
}
impl LanPeers {
@ -865,9 +956,11 @@ impl LanPeers {
}
}
pub fn store(peers: String) {
let f = LanPeers { peers };
if let Err(err) = confy::store_path(Config::file_("_lan_peers"), f) {
pub fn store(peers: &Vec<DiscoveryPeer>) {
let f = LanPeers {
peers: peers.clone(),
};
if let Err(err) = store_path(Config::file_("_lan_peers"), f) {
log::error!("Failed to store lan peers: {}", err);
}
}
@ -881,6 +974,26 @@ impl LanPeers {
}
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct HwCodecConfig {
#[serde(default)]
pub options: HashMap<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)]
mod tests {
use super::*;

View File

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

View File

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

View File

@ -18,6 +18,7 @@ cfg-if = "1.0"
libc = "0.2"
num_cpus = "1.13"
lazy_static = "1.4"
hbb_common = { path = "../hbb_common" }
[dependencies.winapi]
version = "0.3"
@ -48,3 +49,6 @@ tracing = { version = "0.1", optional = true }
gstreamer = { version = "0.16", optional = true }
gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
gstreamer-video = { version = "0.16", optional = true }
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
hwcodec = { git = "https://github.com/21pages/hwcodec", optional = true }

View File

@ -4,7 +4,7 @@ extern crate scrap;
use std::fs::File;
#[cfg(windows)]
use scrap::CapturerMag;
use scrap::{CapturerMag, TraitCapturer};
use scrap::{i420_to_rgb, Display};
fn main() {
@ -21,6 +21,8 @@ fn get_display(i: usize) -> Display {
#[cfg(windows)]
fn record(i: usize) {
use std::time::Duration;
for d in Display::all().unwrap() {
println!("{:?} {} {}", d.origin(), d.width(), d.height());
}
@ -40,7 +42,7 @@ fn record(i: usize) {
println!("Filter window for cls {} name {}", wnd_cls, wnd_name);
}
let frame = capture_mag.frame(0).unwrap();
let frame = capture_mag.frame(Duration::from_millis(0)).unwrap();
println!("Capture data len: {}, Saving...", frame.len());
let mut bitflipped = Vec::with_capacity(w * h * 4);
@ -76,7 +78,7 @@ fn record(i: usize) {
println!("Filter window for cls {} title {}", wnd_cls, wnd_title);
}
let buffer = capture_mag.frame(0).unwrap();
let buffer = capture_mag.frame(Duration::from_millis(0)).unwrap();
println!("Capture data len: {}, Saving...", buffer.len());
let mut frame = Default::default();

View File

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

View File

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

View File

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

View File

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

View File

@ -1,536 +1,372 @@
// https://github.com/astraw/vpx-encode
// https://github.com/astraw/env-libvpx-sys
// https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs
use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *};
use std::os::raw::{c_int, c_uint};
use std::{ptr, slice};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum VideoCodecId {
VP8,
VP9,
}
impl Default for VideoCodecId {
fn default() -> VideoCodecId {
VideoCodecId::VP9
}
}
pub struct Encoder {
ctx: vpx_codec_ctx_t,
width: usize,
height: usize,
}
pub struct Decoder {
ctx: vpx_codec_ctx_t,
}
#[derive(Debug)]
pub enum Error {
FailedCall(String),
BadPtr(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{:?}", self)
}
}
impl std::error::Error for Error {}
pub type Result<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 {
pub fn new(config: &Config, num_threads: u32) -> Result<Self> {
let i;
if cfg!(feature = "VP8") {
i = match config.codec {
VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()),
VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()),
};
} else {
i = call_vpx_ptr!(vpx_codec_vp9_cx());
}
let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0));
// https://www.webmproject.org/docs/encoder-parameters/
// default: c.rc_min_quantizer = 0, c.rc_max_quantizer = 63
// try rc_resize_allowed later
c.g_w = config.width;
c.g_h = config.height;
c.g_timebase.num = config.timebase[0];
c.g_timebase.den = config.timebase[1];
c.rc_target_bitrate = config.bitrate;
c.rc_undershoot_pct = 95;
c.rc_dropframe_thresh = 25;
if config.rc_min_quantizer > 0 {
c.rc_min_quantizer = config.rc_min_quantizer;
}
if config.rc_max_quantizer > 0 {
c.rc_max_quantizer = config.rc_max_quantizer;
}
let mut speed = config.speed;
if speed <= 0 {
speed = 6;
}
c.g_threads = if num_threads == 0 {
num_cpus::get() as _
} else {
num_threads
};
c.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
// https://developers.google.com/media/vp9/bitrate-modes/
// Constant Bitrate mode (CBR) is recommended for live streaming with VP9.
c.rc_end_usage = vpx_rc_mode::VPX_CBR;
// c.kf_min_dist = 0;
// c.kf_max_dist = 999999;
c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot
/*
VPX encoder支持two-pass 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)]
pub struct EncodeFrame<'a> {
/// Compressed data.
pub data: &'a [u8],
/// Whether the frame is a keyframe.
pub key: bool,
/// Presentation timestamp (in timebase units).
pub pts: i64,
}
#[derive(Clone, Copy, Debug)]
pub struct Config {
/// The width (in pixels).
pub width: c_uint,
/// The height (in pixels).
pub height: c_uint,
/// The timebase numerator and denominator (in seconds).
pub timebase: [c_int; 2],
/// The target bitrate (in kilobits per second).
pub bitrate: c_uint,
/// The codec
pub codec: VideoCodecId,
pub rc_min_quantizer: u32,
pub rc_max_quantizer: u32,
pub speed: i32,
}
pub struct EncodeFrames<'a> {
ctx: &'a mut vpx_codec_ctx_t,
iter: vpx_codec_iter_t,
}
impl<'a> Iterator for EncodeFrames<'a> {
type Item = EncodeFrame<'a>;
fn next(&mut self) -> Option<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 Decoder {
/// Create a new decoder
///
/// # Errors
///
/// The function may fail if the underlying libvpx does not provide
/// the VP9 decoder.
pub fn new(codec: VideoCodecId, num_threads: u32) -> Result<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 codec {
VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()),
VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()),
};
} else {
i = call_vpx_ptr!(vpx_codec_vp9_dx());
}
let mut ctx = Default::default();
let cfg = vpx_codec_dec_cfg_t {
threads: if num_threads == 0 {
num_cpus::get() as _
} else {
num_threads
},
w: 0,
h: 0,
};
/*
unsafe {
println!("{}", vpx_codec_get_caps(i));
}
*/
call_vpx!(vpx_codec_dec_init_ver(
&mut ctx,
i,
&cfg,
0,
VPX_DECODER_ABI_VERSION as _,
));
Ok(Self { ctx })
}
pub fn decode2rgb(&mut self, data: &[u8], rgba: bool) -> Result<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 Decoder {
fn drop(&mut self) {
unsafe {
let result = vpx_codec_destroy(&mut self.ctx);
if result != VPX_CODEC_OK {
panic!("failed to destroy vpx codec");
}
}
}
}
pub struct DecodeFrames<'a> {
ctx: &'a mut vpx_codec_ctx_t,
iter: vpx_codec_iter_t,
}
impl<'a> Iterator for DecodeFrames<'a> {
type Item = Image;
fn next(&mut self) -> Option<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 {}
use std::ops::{Deref, DerefMut};
#[cfg(feature = "hwcodec")]
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
#[cfg(feature = "hwcodec")]
use crate::hwcodec::*;
use crate::vpxcodec::*;
use hbb_common::{
anyhow::anyhow,
log,
message_proto::{video_frame, EncodedVideoFrames, Message, VideoCodecState},
ResultType,
};
#[cfg(feature = "hwcodec")]
use hbb_common::{
config::{Config2, PeerConfig},
lazy_static,
message_proto::video_codec_state::PerferCodec,
};
#[cfg(feature = "hwcodec")]
lazy_static::lazy_static! {
static ref PEER_DECODER_STATES: Arc<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,
}
#[derive(Debug, Clone)]
pub enum EncoderCfg {
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 codec: Box<dyn EncoderApi>,
}
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 {
vpx: VpxDecoder,
#[cfg(feature = "hwcodec")]
hw: HwDecoders,
#[cfg(feature = "hwcodec")]
i420: Vec<u8>,
}
#[derive(Debug, Clone)]
pub enum EncoderUpdate {
State(VideoCodecState),
Remove,
DisableHwIfNotExist,
}
impl Encoder {
pub fn new(config: EncoderCfg) -> ResultType<Encoder> {
log::info!("new encoder:{:?}", config);
match config {
EncoderCfg::VPX(_) => Ok(Encoder {
codec: Box::new(VpxEncoder::new(config)?),
}),
#[cfg(feature = "hwcodec")]
EncoderCfg::HW(_) => match HwEncoder::new(config) {
Ok(hw) => Ok(Encoder {
codec: Box::new(hw),
}),
Err(e) => {
check_config_process(true);
Err(e)
}
},
#[cfg(not(feature = "hwcodec"))]
_ => Err(anyhow!("unsupported encoder type")),
}
}
// TODO
pub fn update_video_encoder(id: i32, update: EncoderUpdate) {
#[cfg(feature = "hwcodec")]
{
let mut states = PEER_DECODER_STATES.lock().unwrap();
match update {
EncoderUpdate::State(state) => {
states.insert(id, state);
}
EncoderUpdate::Remove => {
states.remove(&id);
}
EncoderUpdate::DisableHwIfNotExist => {
if !states.contains_key(&id) {
states.insert(id, VideoCodecState::default());
}
}
}
let name = HwEncoder::current_name();
if states.len() > 0 {
let best = HwEncoder::best();
let enabled_h264 = best.h264.is_some()
&& states.len() > 0
&& states.iter().all(|(_, s)| s.score_h264 > 0);
let enabled_h265 = best.h265.is_some()
&& states.len() > 0
&& states.iter().all(|(_, s)| s.score_h265 > 0);
// Preference first
let mut preference = PerferCodec::Auto;
let preferences: Vec<_> = states
.iter()
.filter(|(_, s)| {
s.perfer == PerferCodec::VPX.into()
|| s.perfer == PerferCodec::H264.into() && enabled_h264
|| s.perfer == PerferCodec::H265.into() && enabled_h265
})
.map(|(_, s)| s.perfer)
.collect();
if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) {
preference = preferences[0].enum_value_or(PerferCodec::Auto);
}
match preference {
PerferCodec::VPX => *name.lock().unwrap() = None,
PerferCodec::H264 => {
*name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name))
}
PerferCodec::H265 => {
*name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name))
}
PerferCodec::Auto => {
// score encoder
let mut score_vpx = SCORE_VPX;
let mut score_h264 = best.h264.as_ref().map_or(0, |c| c.score);
let mut score_h265 = best.h265.as_ref().map_or(0, |c| c.score);
// score decoder
score_vpx += states.iter().map(|s| s.1.score_vpx).sum::<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 {
pub fn video_codec_state(_id: &str) -> VideoCodecState {
#[cfg(feature = "hwcodec")]
if check_hwcodec_config() {
let best = HwDecoder::best();
VideoCodecState {
score_vpx: SCORE_VPX,
score_h264: best.h264.map_or(0, |c| c.score),
score_h265: best.h265.map_or(0, |c| c.score),
perfer: Self::codec_preference(_id).into(),
..Default::default()
}
} else {
return VideoCodecState {
score_vpx: SCORE_VPX,
..Default::default()
};
}
#[cfg(not(feature = "hwcodec"))]
VideoCodecState {
score_vpx: SCORE_VPX,
..Default::default()
}
}
pub fn new(config: DecoderCfg) -> Decoder {
let vpx = VpxDecoder::new(config.vpx).unwrap();
Decoder {
vpx,
#[cfg(feature = "hwcodec")]
hw: HwDecoder::new_decoders(),
#[cfg(feature = "hwcodec")]
i420: vec![],
}
}
pub fn handle_video_frame(
&mut self,
frame: &video_frame::Union,
rgb: &mut Vec<u8>,
) -> ResultType<bool> {
match frame {
video_frame::Union::Vp9s(vp9s) => {
Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, rgb)
}
#[cfg(feature = "hwcodec")]
video_frame::Union::H264s(h264s) => {
if let Some(decoder) = &mut self.hw.h264 {
Decoder::handle_hw_video_frame(decoder, h264s, rgb, &mut self.i420)
} else {
Err(anyhow!("don't support h264!"))
}
}
#[cfg(feature = "hwcodec")]
video_frame::Union::H265s(h265s) => {
if let Some(decoder) = &mut self.hw.h265 {
Decoder::handle_hw_video_frame(decoder, h265s, rgb, &mut self.i420)
} else {
Err(anyhow!("don't support h265!"))
}
}
_ => Err(anyhow!("unsupported video frame type!")),
}
}
fn handle_vp9s_video_frame(
decoder: &mut VpxDecoder,
vp9s: &EncodedVideoFrames,
rgb: &mut Vec<u8>,
) -> ResultType<bool> {
let mut last_frame = Image::new();
for vp9 in vp9s.frames.iter() {
for frame in decoder.decode(&vp9.data)? {
drop(last_frame);
last_frame = frame;
}
}
for frame in decoder.flush()? {
drop(last_frame);
last_frame = frame;
}
if last_frame.is_null() {
Ok(false)
} else {
last_frame.rgb(1, true, rgb);
Ok(true)
}
}
#[cfg(feature = "hwcodec")]
fn handle_hw_video_frame(
decoder: &mut HwDecoder,
frames: &EncodedVideoFrames,
rgb: &mut Vec<u8>,
i420: &mut Vec<u8>,
) -> ResultType<bool> {
let mut ret = false;
for h264 in frames.frames.iter() {
for image in decoder.decode(&h264.data)? {
// TODO: just process the last frame
if image.bgra(rgb, i420).is_ok() {
ret = true;
}
}
}
return Ok(ret);
}
#[cfg(feature = "hwcodec")]
fn codec_preference(id: &str) -> PerferCodec {
let codec = PeerConfig::load(id)
.options
.get("codec-preference")
.map_or("".to_owned(), |c| c.to_owned());
if codec == "vp9" {
PerferCodec::VPX
} else if codec == "h264" {
PerferCodec::H264
} else if codec == "h265" {
PerferCodec::H265
} else {
PerferCodec::Auto
}
}
}
#[cfg(feature = "hwcodec")]
fn check_hwcodec_config() -> bool {
if let Some(v) = Config2::get().options.get("enable-hwcodec") {
return v != "N";
}
return true; // default is true
}

View File

@ -49,6 +49,17 @@ extern "C" {
height: c_int,
) -> c_int;
pub fn ARGBToNV12(
src_bgra: *const u8,
src_stride_bgra: c_int,
dst_y: *mut u8,
dst_stride_y: c_int,
dst_uv: *mut u8,
dst_stride_uv: c_int,
width: c_int,
height: c_int,
) -> c_int;
pub fn NV12ToI420(
src_y: *const u8,
src_stride_y: c_int,
@ -91,6 +102,17 @@ extern "C" {
width: c_int,
height: c_int,
) -> c_int;
pub fn NV12ToARGB(
src_y: *const u8,
src_stride_y: c_int,
src_uv: *const u8,
src_stride_uv: c_int,
dst_rgba: *mut u8,
dst_stride_rgba: c_int,
width: c_int,
height: c_int,
) -> c_int;
}
// https://github.com/webmproject/libvpx/blob/master/vpx/src/vpx_image.c
@ -220,3 +242,195 @@ pub unsafe fn nv12_to_i420(
height as _,
);
}
#[cfg(feature = "hwcodec")]
pub mod hw {
use hbb_common::{anyhow::anyhow, ResultType};
#[cfg(target_os = "windows")]
use hwcodec::{ffmpeg::ffmpeg_linesize_offset_length, AVPixelFormat};
pub fn hw_bgra_to_i420(
width: usize,
height: usize,
stride: &[i32],
offset: &[i32],
length: i32,
src: &[u8],
dst: &mut Vec<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 std::io::ErrorKind::{NotFound, TimedOut, WouldBlock};
use std::{io, ops};
use crate::{common::TraitCapturer, dxgi};
use std::{
io::{
self,
ErrorKind::{NotFound, TimedOut, WouldBlock},
},
ops,
time::Duration,
};
pub struct Capturer {
inner: dxgi::Capturer,
@ -20,14 +26,6 @@ impl Capturer {
})
}
pub fn is_gdi(&self) -> bool {
self.inner.is_gdi()
}
pub fn set_gdi(&mut self) -> bool {
self.inner.set_gdi()
}
pub fn cancel_gdi(&mut self) {
self.inner.cancel_gdi()
}
@ -39,14 +37,28 @@ impl Capturer {
pub fn height(&self) -> usize {
self.height
}
}
pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result<Frame<'a>> {
match self.inner.frame(timeout_ms) {
impl TraitCapturer for Capturer {
fn set_use_yuv(&mut self, use_yuv: bool) {
self.inner.set_use_yuv(use_yuv);
}
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> {
match self.inner.frame(timeout.as_millis() as _) {
Ok(frame) => Ok(Frame(frame)),
Err(ref error) if error.kind() == TimedOut => Err(WouldBlock.into()),
Err(error) => Err(error),
}
}
fn is_gdi(&self) -> bool {
self.inner.is_gdi()
}
fn set_gdi(&mut self) -> bool {
self.inner.set_gdi()
}
}
pub struct Frame<'a>(&'a [u8]);
@ -128,6 +140,7 @@ impl CapturerMag {
data: Vec::new(),
})
}
pub fn exclude(&mut self, cls: &str, name: &str) -> io::Result<bool> {
self.inner.exclude(cls, name)
}
@ -135,8 +148,23 @@ impl CapturerMag {
pub fn get_rect(&self) -> ((i32, i32), usize, usize) {
self.inner.get_rect()
}
pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result<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)?;
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::{
wayland,
x11::{self, Frame},
TraitCapturer,
};
use std::io;
use std::{io, time::Duration};
pub enum Capturer {
X11(x11::Capturer),
@ -30,11 +31,20 @@ impl Capturer {
Capturer::WAYLAND(d) => d.height(),
}
}
}
pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result<Frame<'a>> {
impl TraitCapturer for Capturer {
fn set_use_yuv(&mut self, use_yuv: bool) {
match self {
Capturer::X11(d) => d.frame(timeout_ms),
Capturer::WAYLAND(d) => d.frame(timeout_ms),
Capturer::X11(d) => d.set_use_yuv(use_yuv),
Capturer::WAYLAND(d) => d.set_use_yuv(use_yuv),
}
}
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<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),
}
#[inline]
fn is_wayland() -> bool {
std::env::var("IS_WAYLAND").is_ok()
|| std::env::var("XDG_SESSION_TYPE") == Ok("wayland".to_owned())
}
impl Display {
pub fn primary() -> io::Result<Display> {
Ok(if is_wayland() {
Display::WAYLAND(wayland::Display::primary()?)
} else {
Ok(if super::is_x11() {
Display::X11(x11::Display::primary()?)
} else {
Display::WAYLAND(wayland::Display::primary()?)
})
}
pub fn all() -> io::Result<Vec<Display>> {
Ok(if is_wayland() {
wayland::Display::all()?
.drain(..)
.map(|x| Display::WAYLAND(x))
.collect()
} else {
Ok(if super::is_x11() {
x11::Display::all()?
.drain(..)
.map(|x| Display::X11(x))
.collect()
} else {
wayland::Display::all()?
.drain(..)
.map(|x| Display::WAYLAND(x))
.collect()
})
}

View File

@ -1,4 +1,4 @@
pub use self::codec::*;
pub use self::vpxcodec::*;
cfg_if! {
if #[cfg(quartz)] {
@ -11,6 +11,7 @@ cfg_if! {
mod wayland;
mod x11;
pub use self::linux::*;
pub use self::x11::Frame;
} else {
mod x11;
pub use self::x11::*;
@ -29,8 +30,12 @@ cfg_if! {
pub mod codec;
mod convert;
#[cfg(feature = "hwcodec")]
pub mod hwcodec;
pub mod vpxcodec;
pub use self::convert::*;
pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller
pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer
mod vpx;
@ -44,3 +49,19 @@ pub fn would_block_if_equal(old: &mut Vec<u128>, b: &[u8]) -> std::io::Result<()
old.copy_from_slice(b);
Ok(())
}
pub trait TraitCapturer {
fn set_use_yuv(&mut self, use_yuv: bool);
fn frame<'a>(&'a mut self, timeout: std::time::Duration) -> std::io::Result<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 {
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() {
Ok(mut handle) => {
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 std::io;
use std::{io, time::Duration};
pub struct Capturer(Display, Box<dyn Recorder>, bool, Vec<u8>);
@ -21,9 +21,15 @@ impl Capturer {
pub fn height(&self) -> usize {
self.0.height()
}
}
pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result<Frame<'a>> {
match self.1.capture(timeout_ms as _).map_err(map_err)? {
impl TraitCapturer for Capturer {
fn set_use_yuv(&mut self, use_yuv: bool) {
self.2 = use_yuv;
}
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> {
match self.1.capture(timeout.as_millis() as _).map_err(map_err)? {
PixelProvider::BGR0(w, h, x) => Ok(Frame(if self.2 {
crate::common::bgra_to_i420(w as _, h as _, &x, &mut self.3);
&self.3[..]

View File

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

View File

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

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 {
self.gdi_capturer.is_some()
}

View File

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

View File

@ -1,31 +1,177 @@
from pynput.keyboard import Key, Controller
from pynput.keyboard._xorg import KeyCode
from pynput._util.xorg import display_manager
import Xlib
from pynput._util.xorg import *
import Xlib
import os
import sys
import socket
from Xlib.ext.xtest import fake_input
from Xlib import X
import Xlib
KeyCode._from_symbol("\0") # test
DEAD_KEYS = {
'`': 65104,
'´': 65105,
'^': 65106,
'~': 65107,
'¯': 65108,
'˘': 65109,
'˙': 65110,
'¨': 65111,
'˚': 65112,
'˝': 65113,
'ˇ': 65114,
'¸': 65115,
'˛': 65116,
'': 65117, # ?
'': 65118, # ?
'': 65119,
'ٜ': 65120,
'': 65121,
' ̛': 65122,
}
def my_keyboard_mapping(display):
"""Generates a mapping from *keysyms* to *key codes* and required
modifier shift states.
:param Xlib.display.Display display: The display for which to retrieve the
keyboard mapping.
:return: the keyboard mapping
"""
mapping = {}
shift_mask = 1 << 0
group_mask = alt_gr_mask(display)
# Iterate over all keysym lists in the keyboard mapping
min_keycode = display.display.info.min_keycode
keycode_count = display.display.info.max_keycode - min_keycode + 1
for index, keysyms in enumerate(display.get_keyboard_mapping(
min_keycode, keycode_count)):
key_code = index + min_keycode
# Normalise the keysym list to yield a tuple containing the two groups
normalized = keysym_normalize(keysyms)
if not normalized:
continue
# Iterate over the groups to extract the shift and modifier state
for groups, group in zip(normalized, (False, True)):
for keysym, shift in zip(groups, (False, True)):
if not keysym:
continue
shift_state = 0 \
| (shift_mask if shift else 0) \
| (group_mask if group else 0)
# !!!: Save all keycode combinations of keysym
if keysym in mapping:
mapping[keysym].append((key_code, shift_state))
else:
mapping[keysym] = [(key_code, shift_state)]
return mapping
class MyController(Controller):
def _handle(self, key, is_press):
"""Resolves a key identifier and sends a keyboard event.
:param event: The *X* keyboard event.
:param int keysym: The keysym to handle.
def _update_keyboard_mapping(self):
"""Updates the keyboard mapping.
"""
keysym = self._keysym(key)
keycode = self._display.keysym_to_keycode(keysym)
with display_manager(self._display) as dm:
self._keyboard_mapping = my_keyboard_mapping(dm)
def send_event(self, event, keycode, shift_state):
with display_manager(self._display) as dm, self.modifiers as modifiers:
# Under certain cimcumstances, such as when running under Xephyr,
# the value returned by dm.get_input_focus is an int
window = dm.get_input_focus().focus
send_event = getattr(
window,
'send_event',
lambda event: dm.send_event(window, event))
send_event(event(
detail=keycode,
state=shift_state | self._shift_mask(modifiers),
time=0,
root=dm.screen().root,
window=window,
same_screen=0,
child=Xlib.X.NONE,
root_x=0, root_y=0, event_x=0, event_y=0))
def fake_input(self, keycode, is_press):
with display_manager(self._display) as dm:
Xlib.ext.xtest.fake_input(
dm,
Xlib.X.KeyPress if is_press else Xlib.X.KeyRelease,
keycode)
def _handle(self, key, is_press):
"""Resolves a key identifier and sends a keyboard event.
:param event: The *X* keyboard event.
:param int keysym: The keysym to handle.
"""
event = Xlib.display.event.KeyPress if is_press \
else Xlib.display.event.KeyRelease
keysym = self._keysym(key)
if key.vk is not None:
keycode = self._display.keysym_to_keycode(key.vk)
self.fake_input(keycode, is_press)
# Otherwise use XSendEvent; we need to use this in the general case to
# work around problems with keyboard layouts
self._emit('_on_fake_event', key, is_press)
return
# Make sure to verify that the key was resolved
if keysym is None:
raise self.InvalidKeyException(key)
# There may be multiple keycodes for keysym in keyboard_mapping
keycode_flag = len(self.keyboard_mapping[keysym]) == 1
if keycode_flag:
keycode, shift_state = self.keyboard_mapping[keysym][0]
else:
keycode, shift_state = self._display.keysym_to_keycode(keysym), 0
keycode_set = set(map(lambda x: x[0], self.keyboard_mapping[keysym]))
# The keycode of the dead key is inconsistent, The keysym has multiple combinations of a keycode.
if keycode != self._display.keysym_to_keycode(keysym) \
or (keycode_flag == False and keycode == list(keycode_set)[0] and len(keycode_set) == 1):
deakkey_chr = str(key).replace("'", '')
keysym = DEAD_KEYS[deakkey_chr]
keycode, shift_state = self.keyboard_mapping[keysym][0]
# If the key has a virtual key code, use that immediately with
# fake_input; fake input,being an X server extension, has access to
# more internal state that we do
try:
with self.modifiers as modifiers:
alt_gr = Key.alt_gr in modifiers
# !!!: Send_event can't support lock screen, this condition cann't be modified
if alt_gr:
self.send_event(
event, keycode, shift_state)
else:
self.fake_input(keycode, is_press)
except KeyError:
with self._borrow_lock:
keycode, index, count = self._borrows[keysym]
self._send_key(
event,
keycode,
index_to_shift(self._display, index))
count += 1 if is_press else -1
self._borrows[keysym] = (keycode, index, count)
# Notify any running listeners
self._emit('_on_fake_event', key, is_press)
keyboard = MyController()
@ -82,7 +228,7 @@ def loop():
else:
keyboard.release(name)
except Exception as e:
print(e)
print('[x] error key',e)
loop()

View File

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

View File

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

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

View File

@ -3,7 +3,7 @@ use std::{
time::Instant,
};
use hbb_common::log;
use hbb_common::{log, message_proto::{VideoFrame, video_frame}};
const MAX_LATENCY: i64 = 500;
const MIN_LATENCY: i64 = 100;
@ -59,3 +59,33 @@ impl LatencyController {
self.allow_audio
}
}
#[derive(PartialEq, Debug, Clone)]
pub enum CodecFormat {
VP9,
H264,
H265,
Unknown,
}
impl From<&VideoFrame> for CodecFormat {
fn from(it: &VideoFrame) -> Self {
match it.union {
Some(video_frame::Union::Vp9s(_)) => CodecFormat::VP9,
Some(video_frame::Union::H264s(_)) => CodecFormat::H264,
Some(video_frame::Union::H265s(_)) => CodecFormat::H265,
_ => CodecFormat::Unknown,
}
}
}
impl ToString for CodecFormat {
fn to_string(&self) -> String {
match self {
CodecFormat::VP9 => "VP9".into(),
CodecFormat::H264 => "H264".into(),
CodecFormat::H265 => "H265".into(),
CodecFormat::Unknown => "Unknow".into(),
}
}
}

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ use std::{
use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer};
use serde_json::{json, Number, Value};
use hbb_common::ResultType;
use hbb_common::{ResultType, password_security};
use hbb_common::{
config::{self, Config, LocalConfig, PeerConfig, ONLINE},
fs, log,
@ -24,7 +24,8 @@ use crate::ui_interface::{
get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_license,
get_local_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs,
get_uuid, get_version, has_rendezvous_service, is_ok_change_id, post_request, set_local_option,
set_options, set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server,
set_options, set_peer_option, set_socks, store_fav, temporary_password, test_if_valid_server,
using_public_server,
};
fn initialize(app_dir: &str) {
@ -581,8 +582,8 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co
"server_id" => {
res = ui_interface::get_id();
}
"server_password" => {
res = Config::get_password();
"temporary_password" => {
res = password_security::temporary_password();
}
"connect_statue" => {
res = ONLINE
@ -627,7 +628,7 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co
}
}
"uuid" => {
res = base64::encode(crate::get_uuid());
res = base64::encode(get_uuid());
}
_ => {
log::error!("Unknown name of get_by_name: {}", name);
@ -942,13 +943,13 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) {
// }
// }
// Server Side
"update_password" => {
if value.is_empty() {
Config::set_password(&Config::get_auto_password());
} else {
Config::set_password(value);
}
}
// "update_password" => {
// if value.is_empty() {
// Config::set_password(&Config::get_auto_password());
// } else {
// Config::set_password(value);
// }
// }
#[cfg(target_os = "android")]
"chat_server_mode" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, Value>>(value) {

View File

@ -2,6 +2,7 @@ use std::{collections::HashMap, sync::atomic::Ordering};
#[cfg(not(windows))]
use std::{fs::File, io::prelude::*};
use bytes::Bytes;
use parity_tokio_ipc::{
Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes,
};
@ -15,7 +16,7 @@ use hbb_common::{
config::{self, Config, Config2},
futures::StreamExt as _,
futures_util::sink::SinkExt,
log, timeout, tokio,
log, password_security as password, timeout, tokio,
tokio::io::{AsyncRead, AsyncWrite},
tokio_util::codec::Framed,
ResultType,
@ -66,7 +67,7 @@ pub enum FS {
WriteBlock {
id: i32,
file_num: i32,
data: Vec<u8>,
data: Bytes,
compressed: bool,
},
WriteDone {
@ -87,6 +88,47 @@ pub enum FS {
},
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum DataKeyboard {
Sequence(String),
KeyDown(enigo::Key),
KeyUp(enigo::Key),
KeyClick(enigo::Key),
GetKeyState(enigo::Key),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum DataKeyboardResponse {
GetKeyState(bool),
}
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum DataMouse {
MoveTo(i32, i32),
MoveRelative(i32, i32),
Down(enigo::MouseButton),
Up(enigo::MouseButton),
Click(enigo::MouseButton),
ScrollX(i32),
ScrollY(i32),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum DataControl {
Resolution {
minx: i32,
maxx: i32,
miny: i32,
maxy: i32,
},
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum Data {
@ -102,6 +144,7 @@ pub enum Data {
audio: bool,
file: bool,
file_transfer_enabled: bool,
restart: bool,
},
ChatMessage {
text: String,
@ -130,6 +173,15 @@ pub enum Data {
ClipbaordFile(ClipbaordFile),
ClipboardFileEnabled(bool),
PrivacyModeState((i32, PrivacyModeState)),
TestRendezvousServer,
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
Keyboard(DataKeyboard),
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
KeyboardResponse(DataKeyboardResponse),
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
Mouse(DataMouse),
Control(DataControl),
Empty,
}
#[tokio::main(flavor = "current_thread")]
@ -284,12 +336,18 @@ async fn handle(data: Data, stream: &mut Connection) {
let value;
if name == "id" {
value = Some(Config::get_id());
} else if name == "password" {
value = Some(Config::get_password());
} else if name == "temporary-password" {
value = Some(password::temporary_password());
} else if name == "permanent-password" {
value = Some(Config::get_permanent_password());
} else if name == "salt" {
value = Some(Config::get_salt());
} else if name == "rendezvous_server" {
value = Some(Config::get_rendezvous_server());
value = Some(format!(
"{},{}",
Config::get_rendezvous_server(),
Config::get_rendezvous_servers().join(",")
));
} else if name == "rendezvous_servers" {
value = Some(Config::get_rendezvous_servers().join(","));
} else {
@ -301,8 +359,10 @@ async fn handle(data: Data, stream: &mut Connection) {
if name == "id" {
Config::set_key_confirmed(false);
Config::set_id(&value);
} else if name == "password" {
Config::set_password(&value);
} else if name == "temporary-password" {
password::update_temporary_password();
} else if name == "permanent-password" {
Config::set_permanent_password(&value);
} else if name == "salt" {
Config::set_salt(&value);
} else {
@ -339,6 +399,9 @@ async fn handle(data: Data, stream: &mut Connection) {
.await
);
}
Data::TestRendezvousServer => {
crate::test_rendezvous_server();
}
_ => {}
}
}
@ -450,8 +513,8 @@ where
}
}
pub async fn send_raw(&mut self, data: Vec<u8>) -> ResultType<()> {
self.inner.send(bytes::Bytes::from(data)).await?;
pub async fn send_raw(&mut self, data: Bytes) -> ResultType<()> {
self.inner.send(data).await?;
Ok(())
}
@ -492,9 +555,22 @@ pub async fn set_config(name: &str, value: String) -> ResultType<()> {
set_config_async(name, value).await
}
pub fn set_password(v: String) -> ResultType<()> {
Config::set_password(&v);
set_config("password", v)
pub fn update_temporary_password() -> ResultType<()> {
set_config("temporary-password", "".to_owned())
}
pub fn get_permanent_password() -> String {
if let Ok(Some(v)) = get_config("permanent-password") {
Config::set_permanent_password(&v);
v
} else {
Config::get_permanent_password()
}
}
pub fn set_permanent_password(v: String) -> ResultType<()> {
Config::set_permanent_password(&v);
set_config("permanent-password", v)
}
pub fn get_id() -> String {
@ -513,20 +589,17 @@ pub fn get_id() -> String {
}
}
pub fn get_password() -> String {
if let Ok(Some(v)) = get_config("password") {
Config::set_password(&v);
v
} else {
Config::get_password()
}
}
pub async fn get_rendezvous_server(ms_timeout: u64) -> String {
pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec<String>) {
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 {
Config::get_rendezvous_server()
(
Config::get_rendezvous_server(),
Config::get_rendezvous_servers(),
)
}
}
@ -638,3 +711,10 @@ pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> {
.await?;
Ok(())
}
#[tokio::main(flavor = "current_thread")]
pub async fn test_rendezvous_server() -> ResultType<()> {
let mut c = connect(1000, "").await?;
c.send(&Data::TestRendezvousServer).await?;
Ok(())
}

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;
mod cn;
mod cs;
mod da;
mod sk;
mod de;
mod en;
mod es;
mod eo;
mod es;
mod hu;
mod fr;
mod id;
mod it;
mod ptbr;
mod ru;
mod sk;
mod tr;
mod tw;
mod vn;
mod pl;
lazy_static::lazy_static! {
pub static ref LANGS: Value =
json!(vec![
("en", "English"),
("it", "Italiano"),
("fr", "Français"),
("de", "Deutsch"),
("cn", "简体中文"),
("tw", "繁體中文"),
("pt", "Português"),
("es", "Español"),
("hu", "Magyar"),
("ru", "Русский"),
("sk", "Slovenčina"),
("id", "Indonesia"),
("cs", "Čeština"),
("da", "Dansk"),
("eo", "Esperanto"),
("tr", "Türkçe"),
("vn", "Tiếng Việt"),
("pl", "Polski"),
]);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn translate(name: String) -> String {
@ -46,6 +74,7 @@ pub fn translate_locale(name: String, locale: &str) -> String {
"tw" => tw::T.deref(),
"de" => de::T.deref(),
"es" => es::T.deref(),
"hu" => hu::T.deref(),
"ru" => ru::T.deref(),
"eo" => eo::T.deref(),
"id" => id::T.deref(),
@ -56,6 +85,8 @@ pub fn translate_locale(name: String, locale: &str) -> String {
"cs" => cs::T.deref(),
"da" => da::T.deref(),
"sk" => sk::T.deref(),
"vn" => vn::T.deref(),
"pl" => pl::T.deref(),
_ => en::T.deref(),
};
if let Some(v) = m.get(&name as &str) {

View File

@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "关于"),
("Mute", "静音"),
("Audio Input", "音频输入"),
("Enhancements", "增强功能"),
("Hardware Codec", "硬件编解码"),
("Adaptive Bitrate", "自适应码率"),
("ID Server", "ID服务器"),
("Relay Server", "中继服务器"),
("API Server", "API服务器"),
@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "优化反应时间"),
("Custom", "自定义画质"),
("Show remote cursor", "显示远程光标"),
("Show quality monitor", "显示质量监测"),
("Disable clipboard", "禁止剪贴板"),
("Lock after session end", "断开后锁定远程电脑"),
("Insert", "插入"),
@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "退出"),
("In privacy mode", "进入隐私模式"),
("Out privacy mode", "退出隐私模式"),
("Language", "语言"),
("Keep RustDesk background service", "保持RustDesk后台服务"),
("Ignore Battery Optimizations", "忽略电池优化"),
("android_open_battery_optimizations_tip", "如需关闭此功能请在接下来的RustDesk应用设置页面中找到并进入 [电源] 页面,取消勾选 [不受限制]"),
("Connection not allowed", "对方不允许连接"),
("Use temporary password", "使用临时密码"),
("Use permanent password", "使用固定密码"),
("Use both passwords", "同时使用两种密码"),
("Set permanent password", "设置固定密码"),
("Set temporary password length", "设置临时密码长度"),
("Enable Remote Restart", "允许远程重启"),
("Allow remote restart", "允许远程重启"),
("Restart Remote Device", "重启远程电脑"),
("Are you sure you want to restart", "确定要重启"),
("Restarting Remote Device", "正在重启远程设备"),
("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"),
].iter().cloned().collect();
}

View File

@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "O aplikaci"),
("Mute", "Ztlumit"),
("Audio Input", "Vstup zvuku"),
("Enhancements", ""),
("Hardware Codec", ""),
("Adaptive Bitrate", ""),
("ID Server", "Server pro identif."),
("Relay Server", "Předávací (relay) server"),
("API Server", "Server s API rozhraním"),
@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimalizovat pro co nejnižší prodlevu odezvy"),
("Custom", "Uživatelsky určené"),
("Show remote cursor", "Zobrazovat ukazatel myši z protějšku"),
("Show quality monitor", ""),
("Disable clipboard", "Vypnout schránku"),
("Lock after session end", "Po ukončení relace zamknout plochu"),
("Insert", "Vložit"),
@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Vypnutý"),
("In privacy mode", "v režimu soukromí"),
("Out privacy mode", "mimo režim soukromí"),
("Language", ""),
("Keep RustDesk background service", ""),
("Ignore Battery Optimizations", ""),
("android_open_battery_optimizations_tip", ""),
("Connection not allowed", ""),
("Use temporary password", ""),
("Use permanent password", ""),
("Use both passwords", ""),
("Set permanent password", ""),
("Set temporary password length", ""),
("Enable Remote Restart", ""),
("Allow remote restart", ""),
("Restart Remote Device", ""),
("Are you sure you want to restart", ""),
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
].iter().cloned().collect();
}

View File

@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "Omkring"),
("Mute", "Sluk for mikrofonen"),
("Audio Input", "Lydindgang"),
("Enhancements", ""),
("Hardware Codec", ""),
("Adaptive Bitrate", ""),
("ID Server", "identifikations Server"),
("Relay Server", "Relæ Server"),
("API Server", "API Server"),
@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimeret responstid"),
("Custom", "Brugerdefineret"),
("Show remote cursor", "Vis fjernbetjeningskontrolleret markør"),
("Show quality monitor", ""),
("Disable clipboard", "Deaktiver udklipsholder"),
("Lock after session end", "Lås efter afslutningen af fjernstyring"),
("Insert", "Indsæt"),
@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Slukket"),
("In privacy mode", "I databeskyttelsestilstand"),
("Out privacy mode", "Databeskyttelsestilstand fra"),
("Language", ""),
("Keep RustDesk background service", ""),
("Ignore Battery Optimizations", ""),
("android_open_battery_optimizations_tip", ""),
("Connection not allowed", ""),
("Use temporary password", ""),
("Use permanent password", ""),
("Use both passwords", ""),
("Set permanent password", ""),
("Set temporary password length", ""),
("Enable Remote Restart", ""),
("Allow remote restart", ""),
("Restart Remote Device", ""),
("Are you sure you want to restart", ""),
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
].iter().cloned().collect();
}

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."),
("Password", "Passwort"),
("Ready", "Bereit"),
("Established", "Etabliert"),
("Established", "Verbunden"),
("connecting_status", "Verbinden mit dem RustDesk-Netzwerk..."),
("Enable Service", "Verbindungsserver einschalten"),
("Start Service", "Starte Verbindungsserver"),
("Service is running", "Dienst läuft"),
("Service is not running", "Der Verbindungsserver läuft nicht"),
("Enable Service", "Verbindungsserver aktivieren"),
("Start Service", "Starte Vermittlungsdienst"),
("Service is running", "Vermittlungsdienst aktiv"),
("Service is not running", "Vermittlungsdienst deaktiviert"),
("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Verbindung"),
("Control Remote Desktop", "Entfernten Desktop steuern"),
("Control Remote Desktop", "Entfernten PC steuern"),
("Transfer File", "Datei übertragen"),
("Connect", "Verbinden"),
("Recent Sessions", "Letzte Sitzungen"),
@ -21,46 +21,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Confirmation", "Bestätigung"),
("TCP Tunneling", "TCP Tunneln"),
("Remove", "Entfernen"),
("Refresh random password", "Zufälliges Passwort aktualisieren"),
("Set your own password", "Legen Sie Ihr eigenes Passwort fest"),
("Enable Keyboard/Mouse", "Tastatur/Maus einschalten"),
("Enable Clipboard", "Zwischenablage einschalten"),
("Refresh random password", "Neues zufälliges Passwort"),
("Set your own password", "Eigenes Passwort setzen"),
("Enable Keyboard/Mouse", "Tastatur/Maus aktivieren"),
("Enable Clipboard", "Zwischenablage aktivieren"),
("Enable File Transfer", "Dateiübertragung aktivieren"),
("Enable TCP Tunneling", "TCP-Tunneling einschalten"),
("IP Whitelisting", "IP Freigabeliste"),
("ID/Relay Server", "ID/Verbindungsserver"),
("Stop service", "Verbindungsserver ausschalten"),
("Change ID", "ID wechseln"),
("Enable TCP Tunneling", "TCP-Tunneln aktivieren"),
("IP Whitelisting", "IP-Whitelist"),
("ID/Relay Server", "ID/Vermittlungsserver"),
("Stop service", "Vermittlungsdienst deaktivieren"),
("Change ID", "ID ändern"),
("Website", "Webseite"),
("About", "Über"),
("Mute", "Stummschalten"),
("Audio Input", "Audio-Eingang"),
("Enhancements", "Verbesserungen"),
("Hardware Codec", "Hardware-Codec"),
("Adaptive Bitrate", "Adaptive Bitrate"),
("ID Server", "ID Server"),
("Relay Server", "Verbindungsserver Server"),
("API Server", "API Server"),
("Relay Server", "Vermittlungsserver"),
("API Server", "API-Server"),
("invalid_http", "Muss mit http:// oder https:// beginnen"),
("Invalid IP", "Ungültige IP-Adresse"),
("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein. Länge zwischen 6 und 16."),
("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein, Länge zwischen 6 und 16."),
("Invalid format", "Ungültiges Format"),
("server_not_support", "Noch nicht vom Server unterstützt"),
("server_not_support", "Diese Funktion wird noch nicht vom Server unterstützt"),
("Not available", "Nicht verfügbar"),
("Too frequent", "Zu häufig"),
("Cancel", "Abbrechen"),
("Skip", "Überspringen"),
("Close", "Schließen"),
("Retry", "Nochmal versuchen"),
("Retry", "Erneut versuchen"),
("OK", "OK"),
("Password Required", "Passwort erforderlich"),
("Please enter your password", "Bitte geben Sie Ihr Passwort ein"),
("Please enter your password", "Bitte geben Sie das Passwort des entfernten PCs ein."),
("Remember password", "Passwort merken"),
("Wrong Password", "Falsches Passwort"),
("Do you want to enter again?", "Möchten Sie erneut teilnehmen?"),
("Do you want to enter again?", "Erneut verbinden?"),
("Connection Error", "Verbindungsfehler"),
("Error", "Fehler"),
("Reset by the peer", "Zurücksetzen durch die Gegenstelle"),
("Reset by the peer", "Verbindung wurde von der Gegenstelle zurückgesetzt"),
("Connecting...", "Verbinden..."),
("Connection in progress. Please wait.", "Die Verbindung wird hergestellt. Bitte warten Sie."),
("Please try 1 minute later", "Bitte versuchen Sie es 1 Minute später"),
("Please try 1 minute later", "Bitte versuchen Sie es später erneut"),
("Login Error", "Anmeldefehler"),
("Successful", "Erfolgreich"),
("Connected, waiting for image...", "Verbunden, warten auf Bild..."),
@ -72,91 +75,92 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Receive", "Empfangen"),
("Send", "Senden"),
("Refresh File", "Datei aktualisieren"),
("Local", "Lokaler"),
("Remote", "Entfernter"),
("Local", "Lokal"),
("Remote", "Entfernt"),
("Remote Computer", "Entfernter Computer"),
("Local Computer", "Lokaler Computer"),
("Local Computer", "Dieser Computer"),
("Confirm Delete", "Löschen bestätigen"),
("Delete", "Löschen"),
("Properties", "Eigenschaften"),
("Multi Select", "Mehrfachauswahl"),
("Empty Directory", "Leeres Verzeichnis"),
("Not an empty directory", "Kein leeres Verzeichnis"),
("Empty Directory", "Leerer Ordner"),
("Not an empty directory", "Ordner nicht leer"),
("Are you sure you want to delete this file?", "Sind Sie sicher, dass Sie diese Datei löschen wollen?"),
("Are you sure you want to delete this empty directory?", "Sind Sie sicher, dass Sie dieses leere Verzeichnis löschen möchten?"),
("Are you sure you want to delete the file of this directory?", "Sind Sie sicher, dass Sie die Datei dieses Verzeichnisses löschen möchten?"),
("Do this for all conflicts", "Dies gilt für alle Konflikte"),
("Are you sure you want to delete this empty directory?", "Sind Sie sicher, dass Sie diesen leeren Ordner löschen möchten?"),
("Are you sure you want to delete the file of this directory?", "Sind Sie sicher, dass Sie die Datei dieses Ordners löschen möchten?"),
("Do this for all conflicts", "Für alle Konflikte merken"),
("This is irreversible!", "Dies ist irreversibel!"),
("Deleting", "Löschen"),
("files", "Dateien"),
("Waiting", "Warten"),
("Finished", "Fertiggestellt"),
("Speed", "Geschwindigkeit"),
("Custom Image Quality", "Individuelle Bildqualität"),
("Custom Image Quality", "Benutzerdefinierte Bildqualität"),
("Privacy mode", "Datenschutz-Modus"),
("Block user input", "Benutzereingaben blockieren"),
("Unblock user input", "Benutzereingaben freigeben"),
("Adjust Window", "Fenster anpassen"),
("Original", "Original"),
("Shrink", "Geschrumpft"),
("Stretch", "Gestreckt"),
("Good image quality", "Gute Bildqualität"),
("Shrink", "Verkleinern"),
("Stretch", "Strecken"),
("Good image quality", "Schöner"),
("Balanced", "Ausgeglichen"),
("Optimize reaction time", "Optimierte Reaktionszeit"),
("Optimize reaction time", "Schneller"),
("Custom", "Benutzerdefiniert"),
("Show remote cursor", "Ferngesteuerten Cursor anzeigen"),
("Show remote cursor", "Entfernten Cursor anzeigen"),
("Show quality monitor", "Qualitätsüberwachung anzeigen"),
("Disable clipboard", "Zwischenablage deaktivieren"),
("Lock after session end", "Sperren nach Sitzungsende"),
("Insert", "Einfügen"),
("Insert Lock", "Sperre einfügen"),
("Insert Lock", "Win+L (Sperren) senden"),
("Refresh", "Aktualisieren"),
("ID does not exist", "Die ID existiert nicht"),
("Failed to connect to rendezvous server", "Verbindung zum Verbindungsserver fehlgeschlagen"),
("Please try later", "Bitte versuchen Sie es später"),
("Remote desktop is offline", "Entfernter Desktop ist offline"),
("Key mismatch", "Schlüssel nicht übereinstimmend"),
("ID does not exist", "Diese ID existiert nicht"),
("Failed to connect to rendezvous server", "Verbindung zum Vermittlungsserver fehlgeschlagen"),
("Please try later", "Bitte versuchen Sie es später erneut"),
("Remote desktop is offline", "Entfernter PC ist offline"),
("Key mismatch", "Schlüssel stimmt nicht mit Serverschlüssel überein"),
("Timeout", "Zeitüberschreitung"),
("Failed to connect to relay server", "Verbindung zum Verbindungsserver fehlgeschlagen"),
("Failed to connect via rendezvous server", "Verbindung über rendezvous server fehlgeschlagen"),
("Failed to connect via relay server", "Verbindung über den Verbindungsserver ist fehlgeschlagen"),
("Failed to make direct connection to remote desktop", "Direkte Verbindung zum Entfernten-Desktop konnte nicht hergestellt werden"),
("Failed to connect to relay server", "Verbindung zum Vermittlungsserver fehlgeschlagen"),
("Failed to connect via rendezvous server", "Verbindung über Vermittlungsserver ist fehlgeschlagen"),
("Failed to connect via relay server", "Verbindung über Relay-Server ist fehlgeschlagen"),
("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten PC fehlgeschlagen"),
("Set Password", "Passwort festlegen"),
("OS Password", "Betriebssystem-Passwort"),
("install_tip", "Aufgrund der UAC kann RustDesk in manchen Fällen nicht ordnungsgemäß auf der Gegenseite funktionieren. Um UAC zu vermeiden, klicken Sie bitte auf die Schaltfläche unten, um RustDesk auf dem System zu installieren"),
("Click to upgrade", "Zum Upgrade anklicken"),
("Click to upgrade", "Zum Aktualisieren anklicken"),
("Click to download", "Zum Herunterladen klicken"),
("Click to update", "Zum Aktualisieren klicken"),
("Configure", "Konfigurieren"),
("config_acc", "Um Ihren Desktop aus der Ferne zu steuern, müssen Sie RustDesk \"Zugangs\" Rechte erteilen."),
("config_screen", "Um aus der Ferne auf Ihren Desktop zugreifen zu können, müssen Sie RustDesk \"Bildschirm-Aufnahme\" Berechtigungen erteilen."),
("config_acc", "Um Ihren PC aus der Ferne zu steuern, müssen Sie RustDesk Zugriffsrechte erteilen."),
("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk \"Bildschirm-Aufnahme\"-Berechtigung erteilen."),
("Installing ...", "Installiere ..."),
("Install", "Installieren"),
("Installation", "Einrichtung"),
("Installation Path", "Einrichtungs Pfad"),
("Create start menu shortcuts", "Startmenü Verknüpfungen erstellen"),
("Create desktop icon", "Desktop Symbol erstellen"),
("agreement_tip", "Wenn Sie die Einrichtung starten, akzeptieren Sie die Lizenzvereinbarung"),
("Accept and Install", "Akzeptieren und installieren"),
("Installation", "Installation"),
("Installation Path", "Installationspfad"),
("Create start menu shortcuts", "Verknüpfung im Startmenü erstellen"),
("Create desktop icon", "Desktop-Verknüpfung erstellen"),
("agreement_tip", "Durch die Installation akzeptieren Sie die Lizenzvereinbarung"),
("Accept and Install", "Akzeptieren und Installieren"),
("End-user license agreement", "Lizenzvereinbarung für Endbenutzer"),
("Generating ...", "Generierung ..."),
("Your installation is lower version.", "Ihre Installation ist eine niedrigere Version."),
("not_close_tcp_tip", "Schließen Sie dieses Fenster nicht, während Sie den Tunnel benutzen."),
("Listening ...", "Hören ..."),
("Remote Host", "Entfernter Rechner"),
("Generating ...", "Generiere..."),
("Your installation is lower version.", "Ihre Installation ist älter."),
("not_close_tcp_tip", "Schließen Sie dieses Fenster nicht, solange Sie den Tunnel benutzen."),
("Listening ...", "Höre..."),
("Remote Host", "Entfernter PC"),
("Remote Port", "Entfernter Port"),
("Action", "Aktion"),
("Add", "Hinzufügen"),
("Local Port", "Lokaler Port"),
("setup_server_tip", "Für eine schnellere Verbindung, richten Sie bitte Ihren eigenen Verbindungsserver ein"),
("setup_server_tip", "Für eine schnellere Verbindung richten Sie bitte Ihren eigenen Verbindungsserver ein"),
("Too short, at least 6 characters.", "Zu kurz, mindestens 6 Zeichen."),
("The confirmation is not identical.", "Die Bestätigung ist nicht identisch."),
("The confirmation is not identical.", "Die Passwörter sind nicht identisch."),
("Permissions", "Berechtigungen"),
("Accept", "Akzeptieren"),
("Dismiss", "Ablehnen"),
("Disconnect", "Verbindung trennen"),
("Allow using keyboard and mouse", "Erlaubt die Verwendung von Tastatur und Maus"),
("Allow using keyboard and mouse", "Verwendung von Maus und Tastatur zulassen"),
("Allow using clipboard", "Verwendung der Zwischenablage zulassen"),
("Allow hearing sound", "Erlaubt das Hören von Sound"),
("Allow hearing sound", "System-Audio übertragen"),
("Allow file copy and paste", "Kopieren und Einfügen von Dateien zulassen"),
("Connected", "Verbunden"),
("Direct and encrypted connection", "Direkte und verschlüsselte Verbindung"),
@ -167,17 +171,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enter your password", "Geben Sie Ihr Passwort ein"),
("Logging in...", "Anmeldung..."),
("Enable RDP session sharing", "RDP-Sitzungsfreigabe aktivieren"),
("Auto Login", "Automatisches Login (nur gültig, wenn Sie \"Sperren nach Sitzungsende\" eingestellt haben)"),
("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Sperren nach Sitzungsende\" aktiviert haben)"),
("Enable Direct IP Access", "Direkten IP-Zugang aktivieren"),
("Rename", "Umbenennen"),
("Space", "Platz"),
("Space", "Speicherplatz"),
("Create Desktop Shortcut", "Desktop-Verknüpfung erstellen"),
("Change Path", "Pfad ändern"),
("Create Folder", "Ordner erstellen"),
("Please enter the folder name", "Bitte geben Sie den Ordnernamen ein"),
("Fix it", "Reparieren"),
("Warning", "Warnung"),
("Login screen using Wayland is not supported", "Anmeldebildschirm mit Wayland wird nicht unterstützt"),
("Login screen using Wayland is not supported", "Anmeldebildschirm wird mit Wayland nicht unterstützt"),
("Reboot required", "Neustart erforderlich"),
("Unsupported display server ", "Nicht unterstützter Display-Server"),
("x11 expected", "X11 erwartet"),
@ -185,14 +189,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Settings", "Einstellungen"),
("Username", " Benutzername"),
("Invalid port", "Ungültiger Port"),
("Closed manually by the peer", "Vom Peer manuell geschlossen"),
("Enable remote configuration modification", "Änderung der Fernkonfiguration zulassen"),
("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"),
("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"),
("Run without install", "Ohne Installation ausführen"),
("Always connected via relay", "Immer über Verbindungsserver verbunden"),
("Always connect via relay", "Verbindung immer über Verbindungsserver"),
("whitelist_tip", "Nur IPs auf der Freigabeliste können auf mich zugreifen"),
("Login", "Anmeldung"),
("Logout", "Abmeldung"),
("Always connected via relay", "Immer über Relay-Server verbunden"),
("Always connect via relay", "Immer über Relay-Server verbinden"),
("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen"),
("Login", "Anmelden"),
("Logout", "Abmelden"),
("Tags", "Stichworte"),
("Search ID", "Suche ID"),
("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt"),
@ -201,83 +205,99 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Add Tag", "Stichwort hinzufügen"),
("Unselect all tags", "Alle Stichworte abwählen"),
("Network error", "Netzwerkfehler"),
("Username missed", "Benutzername fehlt"),
("Username missed", "Benutzername vergessen"),
("Password missed", "Passwort vergessen"),
("Wrong credentials", "Falsche Anmeldedaten"),
("Edit Tag", "Stichwort bearbeiten"),
("Unremember Password", "Passwort nicht merken"),
("Unremember Password", "Passwort vergessen"),
("Favorites", "Favoriten"),
("Add to Favorites", "Zu Favoriten hinzufügen"),
("Remove from Favorites", "Entferne von Favoriten"),
("Remove from Favorites", "Aus Favoriten entfernen"),
("Empty", "Leer"),
("Invalid folder name", "Ungültiger Ordnername"),
("Socks5 Proxy", "Socks5 Proxy"),
("Hostname", "Rechnername"),
("Discovered", "Gefunden"),
("install_daemon_tip", "Um beim Booten zu starten, müssen Sie den Systemdienst installieren"),
("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein"),
("Remote ID", "Entfernte ID"),
("Paste", "Einfügen"),
("Paste here?", "Hier einfügen?"),
("Are you sure to close the connection?", "Sind Sie sicher, dass Sie die Verbindung schließen wollen?"),
("Are you sure to close the connection?", "Möchten Sie diese Verbindung wirklich trennen?"),
("Download new version", "Neue Version herunterladen"),
("Touch mode", "Touch-Modus"),
("Mouse mode", "Mouse-Modus"),
("One-Finger Tap", "Ein Fingertipp"),
("Left Mouse", "Linke Maus"),
("One-Long Tap", "Tippen Sie mit einem Finger lang"),
("Two-Finger Tap", "Zwei Finger tippen"),
("Right Mouse", "Rechte Maus"),
("One-Finger Move", "Eine Fingerbewegung"),
("Double Tap & Move", "Doppeltippen und verschieben"),
("Mouse Drag", "Maus ziehen"),
("Three-Finger vertically", "Drei Finger vertikal"),
("Mouse mode", "Maus-Modus"),
("One-Finger Tap", "1-Finger-Tipp"),
("Left Mouse", "Linksklick"),
("One-Long Tap", "1-Finger-Halten"),
("Two-Finger Tap", "2-Finger-Tipp"),
("Right Mouse", "Rechtsklick"),
("One-Finger Move", "Einen Finger bewegen"),
("Double Tap & Move", "Doppeltippen und bewegen"),
("Mouse Drag", "Maus bewegen"),
("Three-Finger vertically", "Drei Finger vertikal bewegen"),
("Mouse Wheel", "Mausrad"),
("Two-Finger Move", "Zwei Finger Bewegung"),
("Canvas Move", "Leinwand bewegen"),
("Pinch to Zoom", "Zum Zoomen kneifen"),
("Canvas Zoom", "Leinwand Zoom"),
("Reset canvas", "Anzeige zurücksetzen"),
("No permission of file transfer", "Keine Erlaubnis zur Dateiübertragung"),
("Note", "Notiz"),
("Two-Finger Move", "Zwei Finger bewegen"),
("Canvas Move", "Sichtfeld bewegen"),
("Pinch to Zoom", "2-Finger-Zoom"),
("Canvas Zoom", "Sichtfeld-Zoom"),
("Reset canvas", "Sichtfeld zurücksetzen"),
("No permission of file transfer", "Keine Dateizugriff-Berechtigung"),
("Note", "Anmerkung"),
("Connection", "Verbindung"),
("Share Screen", "Bildschirm freigeben"),
("CLOSE", "NAH DRAN"),
("OPEN", "OFFEN"),
("Chat", "Plaudern"),
("CLOSE", "DEAKTIV."),
("OPEN", "AKTIVIER."),
("Chat", "Chat"),
("Total", "Gesamt"),
("items", "Artikel"),
("items", "Einträge"),
("Selected", "Ausgewählt"),
("Screen Capture", "Bildschirmaufnahme"),
("Input Control", "Eingabesteuerung"),
("Audio Capture", "Audioaufnahme"),
("File Connection", "Dateiverbindung"),
("Screen Capture", "Bildschirmzugr."),
("Input Control", "Eingabezugriff"),
("Audio Capture", "Audiozugriff"),
("File Connection", "Dateizugriff"),
("Screen Connection", "Bildschirmanschluss"),
("Do you accept?", "Akzeptieren Sie?"),
("Do you accept?", "Verbindung zulassen?"),
("Open System Setting", "Systemeinstellung öffnen"),
("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"),
("android_input_permission_tip1", "Damit ein Remote-Gerät Ihr Android-Gerät per Maus oder Berührung steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."),
("android_input_permission_tip1", "Damit ein Remote-Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."),
("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen und geben Sie [Installierte Dienste] ein, schalten Sie den Dienst [RustDesk Input] ein."),
("android_new_connection_tip", "Es wurde eine neue Steuerungsanforderung empfangen, die Ihr aktuelles Gerät steuern möchte."),
("android_service_will_start_tip", "Durch das Einschalten der Bildschirmaufnahme wird der Dienst automatisch gestartet, sodass andere Geräte eine Verbindung von diesem Gerät anfordern können."),
("android_stop_service_tip", "Durch das Schließen des Dienstes werden automatisch alle hergestellten Verbindungen geschlossen."),
("android_version_audio_tip", "Die aktuelle Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher."),
("android_start_service_tip", "Tippen Sie auf [Dienst starten] oder ÖFFNEN Sie die Berechtigung [Bildschirmaufnahme], um den Bildschirmfreigabedienst zu starten."),
("android_new_connection_tip", "möchte ihr Gerät steuern."),
("android_service_will_start_tip", "Durch das Aktivieren der Bildschirmfreigabe wird der Dienst automatisch gestartet, sodass andere Geräte dieses Android-Gerät steuern können."),
("android_stop_service_tip", "Durch das Deaktivieren des Dienstes werden automatisch alle hergestellten Verbindungen getrennt."),
("android_version_audio_tip", "Ihre Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher, falls möglich."),
("android_start_service_tip", "Tippen Sie auf [Dienst aktivieren] oder aktivieren Sie die Berechtigung [Bildschirmzugr.], um den Bildschirmfreigabedienst zu starten."),
("Account", "Konto"),
("Overwrite", "Überschreiben"),
("This file exists, skip or overwrite this file?", "Diese Datei existiert, diese Datei überspringen oder überschreiben?"),
("Quit", "Aufhören"),
("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"),
("This file exists, skip or overwrite this file?", "Diese Datei existiert; überspringen oder überschreiben?"),
("Quit", "Beenden"),
("doc_mac_permission", "https://rustdesk.com/docs/de/manual/mac/#berechtigungen-aktivieren"),
("Help", "Hilfe"),
("Failed", "Gescheitert"),
("Failed", "Fehlgeschlagen"),
("Succeeded", "Erfolgreich"),
("Someone turns on privacy mode, exit", "Jemand aktiviert den Datenschutzmodus, beenden"),
("Someone turns on privacy mode, exit", "Jemand hat den Datenschutzmodus aktiviert, beende..."),
("Unsupported", "Nicht unterstützt"),
("Peer denied", "Peer verweigert"),
("Peer denied", "Die Gegenstelle hat die Verbindung abgelehnt"),
("Please install plugins", "Bitte installieren Sie Plugins"),
("Peer exit", "Peer-Ausgang"),
("Peer exit", "Die Gegenstelle hat die Verbindung getrennt"),
("Failed to turn off", "Ausschalten fehlgeschlagen"),
("Turned off", "Ausgeschaltet"),
("In privacy mode", "im Datenschutzmodus"),
("Out privacy mode", "Datenschutzmodus aus"),
("In privacy mode", "Datenschutzmodus aktivieren"),
("Out privacy mode", "Datenschutzmodus deaktivieren"),
("Language", "Sprache"),
("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"),
("Ignore Battery Optimizations", "Batterieoptimierung ignorieren"),
("android_open_battery_optimizations_tip", "Möchten Sie die Batterieopimierungs-Einstellungen öffnen?"),
("Connection not allowed", "Verbindung abgelehnt"),
("Use temporary password", "Temporäres Passwort verwenden"),
("Use permanent password", "Dauerhaftes Passwort verwenden"),
("Use both passwords", "Beide Passwörter verwenden"),
("Set permanent password", "Dauerhaftes Passwort setzen"),
("Set temporary password length", "Länge des temporären Passworts setzen"),
("Enable Remote Restart", "Entfernten Neustart aktivieren"),
("Allow remote restart", "Entfernten Neustart erlauben"),
("Restart Remote Device", "Entferntes Gerät neu starten"),
("Are you sure you want to restart", "Möchten Sie das entfernte Gerät wirklich neu starten?"),
("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"),
("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."),
].iter().cloned().collect();
}

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_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"),
("server_not_support", "Not yet supported by the server"),
("android_open_battery_optimizations_tip", "If you want to disable this feature, please go to the next RustDesk application settings page, find and enter [Battery], Uncheck [Unrestricted]"),
("remote_restarting_tip", "Remote device is restarting, please close this message box and reconnect with permanent password after a while"),
].iter().cloned().collect();
}

View File

@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "Pri"),
("Mute", "Muta"),
("Audio Input", "Aŭdia enigo"),
("Enhancements", ""),
("Hardware Codec", ""),
("Adaptive Bitrate", ""),
("ID Server", "Servilo de identigiloj"),
("Relay Server", "Relajsa servilo"),
("API Server", "Servilo de API"),
@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimigi reakcia tempo"),
("Custom", "Personigi bilda kvalito"),
("Show remote cursor", "Montri foran kursoron"),
("Show quality monitor", ""),
("Disable clipboard", "Malebligi poŝon"),
("Lock after session end", "Ŝlosi foran komputilon post malkonektado"),
("Insert", "Enmeti"),
@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", ""),
("In privacy mode", ""),
("Out privacy mode", ""),
("Language", ""),
("Keep RustDesk background service", ""),
("Ignore Battery Optimizations", ""),
("android_open_battery_optimizations_tip", ""),
("Connection not allowed", ""),
("Use temporary password", ""),
("Use permanent password", ""),
("Use both passwords", ""),
("Set permanent password", ""),
("Set temporary password length", ""),
("Enable Remote Restart", ""),
("Allow remote restart", ""),
("Restart Remote Device", ""),
("Are you sure you want to restart", ""),
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
].iter().cloned().collect();
}

View File

@ -3,15 +3,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
("Status", "Estado"),
("Your Desktop", "Tu escritorio"),
("desk_tip", "Puoi accedere al tuo desktop usando l'ID e la password riportati qui."),
("desk_tip", "Tu escritorio puede ser ingresado con esta ID y contraseña."),
("Password", "Contraseña"),
("Ready", "Listo"),
("Established", "Establecido"),
("connecting_status", "Conexión a la red RustDesk en progreso..."),
("Enable Service", "Habilitar Servicio"),
("Start Service", "Iniciar Servicio"),
("Service is running", "Servicio se está ejecutando"),
("Service is not running", "Servicio no se está ejecutando"),
("Service is running", "El servicio se está ejecutando"),
("Service is not running", "El servicio no se está ejecutando"),
("not_ready_status", "No está listo. Comprueba tu conexión"),
("Control Remote Desktop", "Controlar Escritorio Remoto"),
("Transfer File", "Transferir archivo"),
@ -27,14 +27,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable Clipboard", "Habilitar portapapeles"),
("Enable File Transfer", "Habilitar transferencia de archivos"),
("Enable TCP Tunneling", "Habilitar tunel TCP"),
("IP Whitelisting", "Lista blanca IP"),
("ID/Relay Server", "Servidor de ID/Relay"),
("IP Whitelisting", "Lista blanca de IP"),
("ID/Relay Server", "Servidor ID/Relay"),
("Stop service", "Parar servicio"),
("Change ID", "Cambiar identificación"),
("Change ID", "Cambiar ID"),
("Website", "Sitio web"),
("About", "Sobre"),
("About", "Acerca de"),
("Mute", "Silencio"),
("Audio Input", "Entrada de audio"),
("Enhancements", "mejoras"),
("Hardware Codec", ""),
("Adaptive Bitrate", ""),
("ID Server", "ID server"),
("Relay Server", "Server relay"),
("API Server", "Server API"),
@ -43,12 +46,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9 e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 a 16 caracteres."),
("Invalid format", "Formato inválido"),
("server_not_support", "Aún no es compatible con el servidor"),
("Not available", "Indisponible"),
("Not available", "No disponible"),
("Too frequent", "Demasiado frecuente"),
("Cancel", "Cancelar"),
("Skip", "Saltar"),
("Close", "Cerrar"),
("Retry", "Volver"),
("Retry", "Reintentar"),
("OK", "OK"),
("Password Required", "Se requiere contraseña"),
("Please enter your password", "Por favor, introduzca su contraseña"),
@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimizar el tiempo de reacción"),
("Custom", "Personalizado"),
("Show remote cursor", "Mostrar cursor remoto"),
("Show quality monitor", "Mostrar calidad del monitor"),
("Disable clipboard", "Deshabilitar portapapeles"),
("Lock after session end", "Bloquear después del final de la sesión"),
("Insert", "Insertar"),
@ -113,7 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ID does not exist", "ID no existe"),
("Failed to connect to rendezvous server", "No se pudo conectar al servidor de encuentro"),
("Please try later", "Por favor intente mas tarde"),
("Remote desktop is offline", "El escritorio remoto está fuera de línea"),
("Remote desktop is offline", "El escritorio remoto está desconectado"),
("Key mismatch", "La clave no coincide"),
("Timeout", "Timeout"),
("Failed to connect to relay server", "No se pudo conectar al servidor de retransmisión"),
@ -125,7 +129,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("install_tip", "Debido al Control de cuentas de usuario, es posible que RustDesk no funcione correctamente como escritorio remoto. Para evitar este problema, haga clic en el botón de abajo para instalar RustDesk a nivel de sistema."),
("Click to upgrade", "Clic para actualizar"),
("Click to download", "Clic para descargar"),
("Click to update", "Fare clic per aggiornare"),
("Click to update", "Clic para refrescar"),
("Configure", "Configurar"),
("config_acc", "Para controlar su escritorio desde el exterior, debe otorgar permiso a RustDesk de \"Accesibilidad\"."),
("config_screen", "Para controlar su escritorio desde el exterior, debe otorgar permiso a RustDesk de \"Grabación de pantalla\"."),
@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Apagado"),
("In privacy mode", "En modo de privacidad"),
("Out privacy mode", "Fuera del modo de privacidad"),
("Language", "Idioma"),
("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"),
("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"),
("android_open_battery_optimizations_tip", ""),
("Connection not allowed", "Conexión no disponible"),
("Use temporary password", "Usar contraseña temporal"),
("Use permanent password", "Usar contraseña permamente"),
("Use both passwords", "Usar ambas comtraseñas"),
("Set permanent password", "Establecer contraseña permamente"),
("Set temporary password length", "Establecer largo de contraseña temporal"),
("Enable Remote Restart", "Activar reinicio remoto"),
("Allow remote restart", "Permitir reinicio remoto"),
("Restart Remote Device", "Reiniciar dispositivo"),
("Are you sure you want to restart", "Esta Seguro que desea reiniciar?"),
("Restarting Remote Device", "Reiniciando dispositivo remoto"),
("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."),
].iter().cloned().collect();
}

View File

@ -35,6 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About", "À propos de"),
("Mute", "Muet"),
("Audio Input", "Entrée audio"),
("Enhancements", ""),
("Hardware Codec", ""),
("Adaptive Bitrate", ""),
("ID Server", "Serveur ID"),
("Relay Server", "Serveur relais"),
("API Server", "Serveur API"),
@ -105,6 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Optimiser le temps de réaction"),
("Custom", "Qualité d'image personnalisée"),
("Show remote cursor", "Afficher le curseur distant"),
("Show quality monitor", ""),
("Disable clipboard", "Désactiver le presse-papier"),
("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"),
("Insert", "Insérer"),
@ -279,5 +283,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Éteindre"),
("In privacy mode", "en mode privé"),
("Out privacy mode", "hors mode de confidentialité"),
("Language", "Langue"),
("Keep RustDesk background service", ""),
("Ignore Battery Optimizations", ""),
("android_open_battery_optimizations_tip", ""),
("Connection not allowed", ""),
("Use temporary password", ""),
("Use permanent password", ""),
("Use both passwords", ""),
("Set permanent password", ""),
("Set temporary password length", ""),
("Enable Remote Restart", ""),
("Allow remote restart", ""),
("Restart Remote Device", ""),
("Are you sure you want to restart", ""),
("Restarting Remote Device", ""),
("remote_restarting_tip", ""),
].iter().cloned().collect();
}

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