Merge branch 'abr' into hwcodec

This commit is contained in:
csf 2022-06-27 15:21:31 +08:00
commit 085356c0b3
48 changed files with 854 additions and 312 deletions

12
.github/workflows/winget.yml vendored Normal file
View File

@ -0,0 +1,12 @@
name: Publish to WinGet
on:
release:
types: [released]
jobs:
publish:
runs-on: windows-latest # action can only be run on windows
steps:
- uses: vedantmgoyal2009/winget-releaser@latest
with:
identifier: RustDesk.RustDesk
token: ${{ secrets.WINGET_TOKEN }}

190
README-AR.md Normal file
View File

@ -0,0 +1,190 @@
<p align="center">
<img src="logo-header.svg" alt="RustDesk - Your remote desktop"><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-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>
<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>
[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)
.Rustبرنامج آخر لسطح المكتب عن بعد، مكتوب بـ
يعمل خارج الصندوق، لا حاجة إلى إعدادات. لديك سيطرة كاملة على بياناتك، دون مخاوف بشأن الأمن. يمكنك استخدام خادم
الخاص بنا rendezvous/relay
[جهز لنفسك واحدا](https://rustdesk.com/server), أو
[خاص بك rendezvous/relay أكتب خادم](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
لمساعدتك على ذلك [`CONTRIBUTING.md`](CONTRIBUTING.md) يرحب بمساهمة الجميع. اطلع على RustDesk.
[**؟ RustDesk كيفية يعمل**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
[**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases)
## خوادم مفتوحة ومجانية
فيما يلي الخوادم التي تستخدمها مجانًا، وقد تتغير طوال الوقت. إذا لم تكن قريبًا من أحد هؤلاء، فقد تكون شبكتك بطيئة.
| الموقع | المورد | المواصفات |
| --------- | ------------- | ------------------ |
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
| Singapore | Vultr | 1 VCPU / 1GB RAM |
| Dallas | Vultr | 1 VCPU / 1GB RAM | |
## التبعيات
لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة سطح المكتب تستخدم
بنفسك sciter dynamic library عليك تحميل
[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)
Sciter إلى Flutter سنقوم بترحيل نسخة سطح المكتب من .Flutter تستخدم إصدارات الهاتف المحمول.
## خطوات البناء
- C++ build env و Rust development env قم بإعداد
- بطريقة صحيحة `VCPKG_ROOT` env variable وأعد [vcpkg](https://github.com/microsoft/vcpkg) ثبت
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static`
- Linux/MacOS: `vcpkg install libvpx libyuv opus`
- run `cargo run`
## [البناء](https://rustdesk.com/docs/en/dev/build/)
## 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
```
### pynput package تثبيت
```sh
pip3 install pynput
```
### 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
```
### Fix libvpx (For 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
```
### البناء
```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
```
### X11 (Xorg) إلى Wayland تغيير
افتراضية GNOME session ك Xorg إتبع [هذه](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) الخطوات لإعداد Wayland لا تدعم RustDesk
## Docker طريقة البناء باستخدام
ابدأ باستنساخ المستودع وبناء الكونتاينر:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
docker build -t "rustdesk-builder" .
```
ثم، في كل مرة تحتاج إلى بناء التطبيق، قم بتشغيل الأمر التالي:
```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
```
لاحظ أن البناء الأول قد يستغرق وقتًا أطول قبل تخزين التبعيات، وسيكون البناء اللاحق أسرع. بالإضافة إلى ذلك، إذا كنت بحاجة إلى تحديد وسائط مختلفة لأمر البناء، فيمكنك القيام بذلك في نهاية الأمر بوضع
`<OPTIONAL-ARGS>`
على سبيل المثال، إذا كنت ترغب في بناء إصدار محسن، فستقوم بتشغيل الأمر أعلاه متبوعًا بـ
`--release`
:سيكون الملف القابل للتنفيذ الناتج متاحًا في مجلد تارغت، ويمكن تشغيله باستخدام
```sh
target/debug/rustdesk
```
:أو في حال قمت ببناء إصدار محسن
```sh
target/release/rustdesk
```
RustDesk يرجى التأكد من أنك تنفذ هذه الأوامر من جذر مستودع
وإلا فقد لا يتمكن التطبيق من العثور على الموارد المطلوبة. لاحظ أيضًا أن الأوامر الفرعية الأخرى مثل
`install` أو `run`
لا يتم دعمها حاليًا عبر هذه الطريقة لأنها ستقوم بتثبيت أو تشغيل البرنامج داخل الكونتاينر بدلاً من الهوست.
## هيكل الملف
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: وظائف لنقل الملفات، وبعض وظائف المرافق الأخرى tcp/udp، protobuf ترميز الفيديو، إعدادات
- **[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)**: واجهة المستخدم الرسومية
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: خدمات الصوت/الحافظة/المدخلات/الفيديو، ووصلات الشبكة
- **[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)**: أو المنقول عن بُعد (TCP hole punching) انتظر الاتصال المباشر [rustdesk-server](https://github.com/rustdesk/rustdesk-server) الإتصال ب
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: رمز خاص بكل منصة
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: رمز الهاتف المحمول
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**:Flutter لعميل الويب الخاص ب Javascript
## لقطات
![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="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Struktura</a> <a href="#file-structure">Struktura</a>
<a href="#snapshot">Ukázky</a><br> <a href="#snapshot">Ukázky</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>]<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>
<b>Potřebujeme Vaši pomoc s překláním textů tohoto ČTIMNE, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">uživatelského rozhraní aplikace RustDesk</a> a <a href="https://github.com/rustdesk/doc.rustdesk.com">dokumentace k ní</a> do vašeho jazyka</b> <b>Potřebujeme Vaši pomoc s překláním textů tohoto ČTIMNE, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">uživatelského rozhraní aplikace RustDesk</a> a <a href="https://github.com/rustdesk/doc.rustdesk.com">dokumentace k ní</a> do vašeho jazyka</b>
</p> </p>

View File

@ -5,7 +5,7 @@
<a href="#auf-docker-kompilieren">Docker</a> <a href="#auf-docker-kompilieren">Docker</a>
<a href="#dateistruktur">Dateistruktur</a> <a href="#dateistruktur">Dateistruktur</a>
<a href="#screenshots">Screenshots</a><br> <a href="#screenshots">Screenshots</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>]<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>
<b>Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren</b> <b>Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren</b>
</p> </p>

View File

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

View File

@ -5,7 +5,7 @@
<a href="#como-compilar-con-docker">Docker</a> <a href="#como-compilar-con-docker">Docker</a>
<a href="#estructura-de-archivos">Estructura</a> <a href="#estructura-de-archivos">Estructura</a>
<a href="#captura-de-pantalla">Captura de pantalla</a><br> <a href="#captura-de-pantalla">Captura de pantalla</a><br>
[<a href="README-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>]<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>
<b>Necesitamos tu ayuda para traducir este README a tu idioma</b> <b>Necesitamos tu ayuda para traducir este README a tu idioma</b>
</p> </p>

View File

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

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Rakenne</a> <a href="#file-structure">Rakenne</a>
<a href="#snapshot">Tilannevedos</a><br> <a href="#snapshot">Tilannevedos</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>]<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>
<b>Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi</b> <b>Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi</b>
</p> </p>

View File

@ -5,7 +5,7 @@
<a href="#comment-construire-avec-docker">Docker</a> - <a href="#comment-construire-avec-docker">Docker</a> -
<a href="#structure-du-projet">Structure</a> - <a href="#structure-du-projet">Structure</a> -
<a href="#images">Images</a><br> <a href="#images">Images</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>]<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>
<b>Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle</b>. <b>Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle</b>.
</p> </p>

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a> <a href="#file-structure">Structure</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-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>]<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>
<b>ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്</b> <b>ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്</b>
</p> </p>

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structuur</a> <a href="#file-structure">Structuur</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>]<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>
<b>We hebben je hulp nodig om deze README te vertalen naar jouw moedertaal</b> <b>We hebben je hulp nodig om deze README te vertalen naar jouw moedertaal</b>
</p> </p>

View File

@ -5,7 +5,7 @@
<a href="#jak-kompilować-za-pomocą-dockera">Docker</a> <a href="#jak-kompilować-za-pomocą-dockera">Docker</a>
<a href="#struktura-plików">Struktura</a> <a href="#struktura-plików">Struktura</a>
<a href="#migawkisnapshoty">Snapshot</a><br> <a href="#migawkisnapshoty">Snapshot</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>]<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>
<b>Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język</b> <b>Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język</b>
</p> </p>

View File

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

View File

@ -5,7 +5,7 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a> <a href="#file-structure">Structure</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-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>]<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>
<b>Нам нужна ваша помощь для перевода этого README и <a href="https://github.com/rustdesk/rustdesk/tree/master/src/rustdesk/tree/master/src/lang">RustDesk UI</a> на ваш родной язык</B> <b>Нам нужна ваша помощь для перевода этого README и <a href="https://github.com/rustdesk/rustdesk/tree/master/src/rustdesk/tree/master/src/lang">RustDesk UI</a> на ваш родной язык</B>
</p> </p>

View File

@ -5,7 +5,7 @@
<a href="#使用Docker编译">Docker</a> <a href="#使用Docker编译">Docker</a>
<a href="#文件结构">结构</a> <a href="#文件结构">结构</a>
<a href="#截图">截图</a><br> <a href="#截图">截图</a><br>
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>]<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>
</p> </p>
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)

View File

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

View File

@ -1 +1,11 @@
Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, <a href="https://rustdesk.com/server">set up your own</a>, or <a href="https://github.com/rustdesk/rustdesk-server-demo">write your own rendezvous/relay server</a>. An open-source remote desktop application, the open source TeamViewer alternative.
Source code: https://github.com/rustdesk/rustdesk
Doc: https://rustdesk.com/docs/en/manual/mobile/
In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Addroid remote control.
In addtion to remote control, you can also transfer files between Android devices and PCs easily with RustDesk.
You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, or self-hosting, or write your own rendezvous/relay server. Self-hosting server is free and open source: https://github.com/rustdesk/rustdesk-server
Please download and install desktop version from: https://rustdesk.com, then you can access and control your desktop from your mobile, or control your mobile from desktop.

View File

@ -1 +1 @@
Yet another remote desktop software An open-source remote desktop application, the open source TeamViewer alternative.

View File

@ -1,3 +1,12 @@
远程桌面软件,开箱即用,无需任何配置。您完全掌控数据,不用担心安全问题。您可以使用我们的注册/中继服务器, 开源远程桌面应用,开源 TeamViewer 替代方案。
或者<a href="https://rustdesk.com/server">自己设置</a> 源代码https://github.com/rustdesk/rustdesk
亦或者<a href="https://github.com/rustdesk/rustdesk-server-demo">开发您的版本</a>。 文档https://rustdesk.com/docs/en/manual/mobile/
为了让远程设备通过鼠标或触摸控制您的 Android 设备,您需要允许 RustDesk 使用“Accessibility”服务RustDesk 使用 AccessibilityService API 来实现 Addroid 远程控制。
除了远程控制,您还可以使用 RustDesk 在 Android 设备和 PC 之间轻松传输文件。
您完全掌控数据,不用担心安全问题。您可以使用我们的注册/中继服务器,或者自建,亦或者开发您的版本。
自托管服务器是免费和开源的https://github.com/rustdesk/rustdesk-server
请从https://rustdesk.com 下载并安装桌面版,然后您可以通过手机访问和控制您的桌面,或从桌面控制您的手机。

View File

@ -1 +1 @@
远程桌面软件 开源远程桌面应用,开源 TeamViewer 替代方案

View File

@ -4,7 +4,7 @@
# Required: # Required:
# 1. set VCPKG_ROOT / ANDROID_NDK path environment variables # 1. set VCPKG_ROOT / ANDROID_NDK path environment variables
# 2. vcpkg initialized # 2. vcpkg initialized
# 3. ndk >= 22 (if ndk< 22 you need to change LD as `export LD=$TOOLCHAIN/bin/aarch64-linux-android-ld`) # 3. ndk, version: 22 (if ndk < 22 you need to change LD as `export LD=$TOOLCHAIN/bin/$NDK_LLVM_TARGET-ld`)
if [ -z "$ANDROID_NDK" ]; then if [ -z "$ANDROID_NDK" ]; then
echo "Failed! Please set ANDROID_NDK" echo "Failed! Please set ANDROID_NDK"
@ -16,66 +16,110 @@ if [ -z "$VCPKG_ROOT" ]; then
exit 1 exit 1
fi fi
PREFIX=$VCPKG_ROOT/installed/arm64-android/ API_LEVEL="21"
echo "*** [Start] Build opus / libyuv from vcpkg" # NDK llvm toolchain
export ANDROID_NDK_HOME=$ANDROID_NDK HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/llvm/prebuilt/`
pushd $VCPKG_ROOT TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG
$VCPKG_ROOT/vcpkg install opus --triplet arm64-android
$VCPKG_ROOT/vcpkg install libyuv --triplet arm64-android function build {
popd ANDROID_ABI=$1
echo "*** [Finished] Build opus / libyuv from vcpkg" VCPKG_TARGET=$2
NDK_LLVM_TARGET=$3
LIBVPX_TARGET=$4
PREFIX=$VCPKG_ROOT/installed/$VCPKG_TARGET/
# 1
echo "*** [$ANDROID_ABI][Start] Build opus / libyuv from vcpkg"
export ANDROID_NDK_HOME=$ANDROID_NDK
pushd $VCPKG_ROOT
$VCPKG_ROOT/vcpkg install opus --triplet $VCPKG_TARGET
$VCPKG_ROOT/vcpkg install libyuv --triplet $VCPKG_TARGET
popd
echo "*** [$ANDROID_ABI][Finished] Build opus / libyuv from vcpkg"
# 2
echo "*** [$ANDROID_ABI][Start] Build libvpx"
pushd build/libvpx
export AR=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-ar
export AS=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-as
export LD=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-ld.gold # if ndk < 22, use aarch64-linux-android-ld
export RANLIB=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-ranlib
export STRIP=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-strip
if [ $NDK_LLVM_TARGET == "arm-linux-androideabi" ]
then
export CC=$TOOLCHAIN/bin/armv7a-linux-androideabi${API_LEVEL}-clang
export CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi${API_LEVEL}-clang++
else
export CC=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}${API_LEVEL}-clang
export CXX=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}${API_LEVEL}-clang++
fi
make clean
./configure --target=$LIBVPX_TARGET \
--enable-pic --disable-vp8 \
--disable-webm-io \
--disable-unit-tests \
--disable-examples \
--disable-libyuv \
--disable-postproc \
--disable-vp8 \
--disable-tools \
--disable-docs \
--prefix=$PREFIX
make -j5
make install
popd
echo "*** [$ANDROID_ABI][Finished] Build libvpx"
# 3
echo "*** [$ANDROID_ABI][Start] Build oboe"
pushd build/oboe
make clean
cmake -DBUILD_SHARED_LIBS=true \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DANDROID_TOOLCHAIN=clang \
-DANDROID_STL=c++_shared \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DANDROID_ABI=$ANDROID_ABI \
-DANDROID_PLATFORM=android-$API_LEVEL
make -j5
make install
mv $PREFIX/lib/$ANDROID_ABI/liboboe.a $PREFIX/lib/
popd
echo "*** [$ANDROID_ABI][Finished] Build oboe"
echo "*** [$ANDROID_ABI][All Finished]"
}
echo "*** [Start] Build libvpx"
git clone -b v1.11.0 --depth=1 https://github.com/webmproject/libvpx.git build/libvpx git clone -b v1.11.0 --depth=1 https://github.com/webmproject/libvpx.git build/libvpx
pushd build/libvpx
export NDK=$ANDROID_NDK
export HOST_TAG=linux-x86_64
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/$HOST_TAG
export AR=$TOOLCHAIN/bin/aarch64-linux-android-ar
export AS=$TOOLCHAIN/bin/aarch64-linux-android-as
export CC=$TOOLCHAIN/bin/aarch64-linux-android21-clang
export CXX=$TOOLCHAIN/bin/aarch64-linux-android21-clang++
export LD=$TOOLCHAIN/bin/aarch64-linux-android-ld.gold # if ndk < 22, use aarch64-linux-android-ld
export RANLIB=$TOOLCHAIN/bin/aarch64-linux-android-ranlib
export STRIP=$TOOLCHAIN/bin/aarch64-linux-android-strip
./configure --target=arm64-android-gcc \
--enable-pic --disable-vp8 \
--disable-webm-io \
--disable-unit-tests \
--disable-examples \
--disable-libyuv \
--disable-postproc \
--disable-vp8 \
--disable-tools \
--disable-docs \
--prefix=$PREFIX
make -j5
make install
popd
echo "*** [Finished] Build libvpx"
echo "*** [Start] Build oboe"
git clone -b 1.6.1 --depth=1 https://github.com/google/oboe build/oboe git clone -b 1.6.1 --depth=1 https://github.com/google/oboe build/oboe
patch -d build/oboe -p1 < ../src/oboe.patch patch -N -d build/oboe -p1 < ../src/oboe.patch
pushd build/oboe
cmake -DBUILD_SHARED_LIBS=true \ # VCPKG_TARGET ANDROID_ABI
-DCMAKE_BUILD_TYPE=RelWithDebInfo \ # arm64-android arm64-v8a
-DANDROID_TOOLCHAIN=clang \ # arm-android armeabi-v7a
-DANDROID_STL=c++_shared \ # x64-android x86_64
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \ # x86-android x86
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DANDROID_ABI=arm64-v8a \ # NDK_LLVM_TARGET
-DANDROID_PLATFORM=android-21 # aarch64-linux-android
make -j5 # arm-linux-androideabi
make install # x86_64-linux-android
mv $PREFIX/lib/arm64-v8a/liboboe.a $PREFIX/lib/ # i686-linux-android
popd
echo "*** [Finished] Build oboe" # LIBVPX_TARGET :
echo "*** [All Finished]" # arm64-android-gcc
# armv7-android-gcc
# x86_64-android-gcc
# x86-android-gcc
# args: ANDROID_ABI VCPKG_TARGET NDK_LLVM_TARGET LIBVPX_TARGET
build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc
build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc
# rm -rf build/libvpx # rm -rf build/libvpx
# rm -rf build/oboe # rm -rf build/oboe

View File

@ -233,7 +233,7 @@ class AccessibilityListener extends StatelessWidget {
} }
}, },
onPointerUp: (evt) { onPointerUp: (evt) {
if (evt.size == 1 && GestureBinding.instance != null) { if (evt.size == 1) {
GestureBinding.instance.handlePointerEvent(PointerUpEvent( GestureBinding.instance.handlePointerEvent(PointerUpEvent(
pointer: evt.pointer + offset, pointer: evt.pointer + offset,
size: 0.1, size: 0.1,
@ -243,7 +243,7 @@ class AccessibilityListener extends StatelessWidget {
} }
}, },
onPointerMove: (evt) { onPointerMove: (evt) {
if (evt.size == 1 && GestureBinding.instance != null) { if (evt.size == 1) {
GestureBinding.instance.handlePointerEvent(PointerMoveEvent( GestureBinding.instance.handlePointerEvent(PointerMoveEvent(
pointer: evt.pointer + offset, pointer: evt.pointer + offset,
size: 0.1, size: 0.1,

View File

@ -593,15 +593,7 @@ class FileFetcher {
tryCompleteTask(String? msg, String? isLocalStr) { tryCompleteTask(String? msg, String? isLocalStr) {
if (msg == null || isLocalStr == null) return; if (msg == null || isLocalStr == null) return;
late final isLocal;
late final tasks; late final tasks;
if (isLocalStr == "true") {
isLocal = true;
} else if (isLocalStr == "false") {
isLocal = false;
} else {
return;
}
try { try {
final fd = FileDirectory.fromJson(jsonDecode(msg)); final fd = FileDirectory.fromJson(jsonDecode(msg));
if (fd.id > 0) { if (fd.id > 0) {

View File

@ -21,7 +21,6 @@ typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
typedef F3 = void Function(Pointer<Utf8>, Pointer<Utf8>); typedef F3 = void Function(Pointer<Utf8>, Pointer<Utf8>);
class PlatformFFI { class PlatformFFI {
static Pointer<RgbaFrame>? _lastRgbaFrame;
static String _dir = ''; static String _dir = '';
static String _homeDir = ''; static String _homeDir = '';
static F2? _getByName; static F2? _getByName;

View File

@ -1,3 +1,5 @@
// ignore_for_file: avoid_web_libraries_in_flutter
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:js'; import 'dart:js';

View File

@ -100,8 +100,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
: InkWell( : InkWell(
onTap: () async { onTap: () async {
final url = _updateUrl + '.apk'; final url = _updateUrl + '.apk';
if (await canLaunch(url)) { if (await canLaunchUrl(Uri.parse(url))) {
await launch(url); await launchUrl(Uri.parse(url));
} }
}, },
child: Container( child: Container(

View File

@ -126,7 +126,7 @@ class _RemotePageState extends State<RemotePage> {
common < oldValue.length && common < oldValue.length &&
common < newValue.length && common < newValue.length &&
newValue[common] == oldValue[common]; newValue[common] == oldValue[common];
++common); ++common) {}
for (i = 0; i < oldValue.length - common; ++i) { for (i = 0; i < oldValue.length - common; ++i) {
FFI.inputKey('VK_BACK'); FFI.inputKey('VK_BACK');
} }

View File

@ -68,8 +68,8 @@ class _SettingsState extends State<SettingsPage> {
tiles: [ tiles: [
SettingsTile.navigation( SettingsTile.navigation(
onPressed: (context) async { onPressed: (context) async {
if (await canLaunch(url)) { if (await canLaunchUrl(Uri.parse(url))) {
await launch(url); await launchUrl(Uri.parse(url));
} }
}, },
title: Text(translate("Version: ") + version), title: Text(translate("Version: ") + version),
@ -105,8 +105,8 @@ void showAbout() {
InkWell( InkWell(
onTap: () async { onTap: () async {
const url = 'https://rustdesk.com/'; const url = 'https://rustdesk.com/';
if (await canLaunch(url)) { if (await canLaunchUrl(Uri.parse(url))) {
await launch(url); await launchUrl(Uri.parse(url));
} }
}, },
child: Padding( child: Padding(
@ -149,7 +149,7 @@ fetch('http://localhost:21114/api/login', {
'uuid': FFI.getByName('uuid') 'uuid': FFI.getByName('uuid')
}; };
try { try {
final response = await http.post(Uri.parse('${url}/api/login'), final response = await http.post(Uri.parse('$url/api/login'),
headers: {"Content-Type": "application/json"}, body: json.encode(body)); headers: {"Content-Type": "application/json"}, body: json.encode(body));
return parseResp(response.body); return parseResp(response.body);
} catch (e) { } catch (e) {
@ -186,7 +186,7 @@ void refreshCurrentUser() async {
'uuid': FFI.getByName('uuid') 'uuid': FFI.getByName('uuid')
}; };
try { try {
final response = await http.post(Uri.parse('${url}/api/currentUser'), final response = await http.post(Uri.parse('$url/api/currentUser'),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": "Bearer $token" "Authorization": "Bearer $token"
@ -212,7 +212,7 @@ void logout() async {
'uuid': FFI.getByName('uuid') 'uuid': FFI.getByName('uuid')
}; };
try { try {
await http.post(Uri.parse('${url}/api/logout'), await http.post(Uri.parse('$url/api/logout'),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": "Bearer $token" "Authorization": "Bearer $token"
@ -242,7 +242,7 @@ String getUrl() {
url = 'http://${tmp[0]}:$port'; url = 'http://${tmp[0]}:$port';
} }
} else { } else {
url = 'http://${url}:21114'; url = 'http://$url:21114';
} }
} }
} }

View File

@ -594,10 +594,7 @@ class _TapTracker {
required this.entry, required this.entry,
required Duration doubleTapMinTime, required Duration doubleTapMinTime,
required this.gestureSettings, required this.gestureSettings,
}) : assert(doubleTapMinTime != null), }) : pointer = event.pointer,
assert(event != null),
assert(event.buttons != null),
pointer = event.pointer,
_initialGlobalPosition = event.position, _initialGlobalPosition = event.position,
initialButtons = event.buttons, initialButtons = event.buttons,
_doubleTapMinTimeCountdown = _doubleTapMinTimeCountdown =
@ -643,7 +640,7 @@ class _TapTracker {
/// CountdownZoned tracks whether the specified duration has elapsed since /// CountdownZoned tracks whether the specified duration has elapsed since
/// creation, honoring [Zone]. /// creation, honoring [Zone].
class _CountdownZoned { class _CountdownZoned {
_CountdownZoned({required Duration duration}) : assert(duration != null) { _CountdownZoned({required Duration duration}) {
Timer(duration, _onTimeout); Timer(duration, _onTimeout);
} }

View File

@ -447,9 +447,9 @@ message PermissionInfo {
enum ImageQuality { enum ImageQuality {
NotSet = 0; NotSet = 0;
Low = 2; Low = 50;
Balanced = 3; Balanced = 66;
Best = 4; Best = 100;
} }
message VideoCodecState { message VideoCodecState {
@ -471,7 +471,7 @@ message OptionMessage {
BoolOption show_remote_cursor = 3; BoolOption show_remote_cursor = 3;
BoolOption privacy_mode = 4; BoolOption privacy_mode = 4;
BoolOption block_input = 5; BoolOption block_input = 5;
int32 custom_image_quality = 6; uint32 custom_image_quality = 6;
BoolOption disable_audio = 7; BoolOption disable_audio = 7;
BoolOption disable_clipboard = 8; BoolOption disable_clipboard = 8;
BoolOption enable_file_transfer = 9; BoolOption enable_file_transfer = 9;
@ -481,6 +481,8 @@ message OptionMessage {
message TestDelay { message TestDelay {
int64 time = 1; int64 time = 1;
bool from_client = 2; bool from_client = 2;
uint32 last_delay = 3;
uint32 target_bitrate = 4;
} }
message PublicKey { message PublicKey {

View File

@ -140,6 +140,8 @@ pub struct PeerConfig {
pub disable_clipboard: bool, pub disable_clipboard: bool,
#[serde(default)] #[serde(default)]
pub enable_file_transfer: bool, pub enable_file_transfer: bool,
#[serde(default)]
pub show_quality_monitor: bool,
// the other scalar value must before this // the other scalar value must before this
#[serde(default)] #[serde(default)]

View File

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

View File

@ -50,6 +50,8 @@ pub trait EncoderApi {
fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType<Message>; fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType<Message>;
fn use_yuv(&self) -> bool; fn use_yuv(&self) -> bool;
fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()>;
} }
pub struct DecoderCfg { pub struct DecoderCfg {

View File

@ -119,17 +119,6 @@ impl EncoderApi for VpxEncoder {
c.rc_target_bitrate = config.bitrate; c.rc_target_bitrate = config.bitrate;
c.rc_undershoot_pct = 95; c.rc_undershoot_pct = 95;
c.rc_dropframe_thresh = 25; 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 config.num_threads == 0 { c.g_threads = if config.num_threads == 0 {
num_cpus::get() as _ num_cpus::get() as _
} else { } else {
@ -174,7 +163,7 @@ impl EncoderApi for VpxEncoder {
Higher numbers (7 or 8) will be lower quality but more manageable for lower latency 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. use cases and also for lower CPU power devices such as mobile.
*/ */
call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, speed,)); call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, 7,));
// set row level multi-threading // set row level multi-threading
/* /*
as some people in comments and below have already commented, as some people in comments and below have already commented,
@ -232,6 +221,13 @@ impl EncoderApi for VpxEncoder {
fn use_yuv(&self) -> bool { fn use_yuv(&self) -> bool {
true 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 { impl VpxEncoder {
@ -336,9 +332,6 @@ pub struct VpxEncoderConfig {
pub bitrate: c_uint, pub bitrate: c_uint,
/// The codec /// The codec
pub codec: VpxVideoCodecId, pub codec: VpxVideoCodecId,
pub rc_min_quantizer: u32,
pub rc_max_quantizer: u32,
pub speed: i32,
pub num_threads: u32, pub num_threads: u32,
} }

View File

@ -1,5 +1,5 @@
use crate::x11; use crate::x11;
use std::{io, ops}; use std::{io, ops, time::Duration};
pub struct Capturer(x11::Capturer); pub struct Capturer(x11::Capturer);
@ -16,7 +16,7 @@ impl Capturer {
self.0.display().rect().h as usize self.0.display().rect().h as usize
} }
pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result<Frame<'a>> { pub fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result<Frame<'a>> {
Ok(Frame(self.0.frame()?)) Ok(Frame(self.0.frame()?))
} }
} }

View File

@ -880,6 +880,8 @@ impl LoginConfigHandler {
option.block_input = BoolOption::Yes.into(); option.block_input = BoolOption::Yes.into();
} else if name == "unblock-input" { } else if name == "unblock-input" {
option.block_input = BoolOption::No.into(); option.block_input = BoolOption::No.into();
} else if name == "show-quality-monitor" {
config.show_quality_monitor = !config.show_quality_monitor;
} else { } else {
let v = self.options.get(&name).is_some(); let v = self.options.get(&name).is_some();
if v { if v {
@ -912,15 +914,8 @@ impl LoginConfigHandler {
n += 1; n += 1;
} else if q == "custom" { } else if q == "custom" {
let config = PeerConfig::load(&self.id); let config = PeerConfig::load(&self.id);
let mut it = config.custom_image_quality.iter(); msg.custom_image_quality = config.custom_image_quality[0] as _;
let bitrate = it.next(); n += 1;
let quantizer = it.next();
if let Some(bitrate) = bitrate {
if let Some(quantizer) = quantizer {
msg.custom_image_quality = bitrate << 8 | quantizer;
n += 1;
}
}
} }
if self.get_toggle_option("show-remote-cursor") { if self.get_toggle_option("show-remote-cursor") {
msg.show_remote_cursor = BoolOption::Yes.into(); msg.show_remote_cursor = BoolOption::Yes.into();
@ -987,6 +982,8 @@ impl LoginConfigHandler {
self.config.disable_audio self.config.disable_audio
} else if name == "disable-clipboard" { } else if name == "disable-clipboard" {
self.config.disable_clipboard self.config.disable_clipboard
} else if name == "show-quality-monitor" {
self.config.show_quality_monitor
} else { } else {
!self.get_option(name).is_empty() !self.get_option(name).is_empty()
} }
@ -1008,17 +1005,17 @@ impl LoginConfigHandler {
msg_out msg_out
} }
pub fn save_custom_image_quality(&mut self, bitrate: i32, quantizer: i32) -> Message { pub fn save_custom_image_quality(&mut self, custom_image_quality: u32) -> Message {
let mut misc = Misc::new(); let mut misc = Misc::new();
misc.set_option(OptionMessage { misc.set_option(OptionMessage {
custom_image_quality: bitrate << 8 | quantizer, custom_image_quality,
..Default::default() ..Default::default()
}); });
let mut msg_out = Message::new(); let mut msg_out = Message::new();
msg_out.set_misc(misc); msg_out.set_misc(misc);
let mut config = self.load_config(); let mut config = self.load_config();
config.image_quality = "custom".to_owned(); config.image_quality = "custom".to_owned();
config.custom_image_quality = vec![bitrate, quantizer]; config.custom_image_quality = vec![custom_image_quality as _];
self.save_config(config); self.save_config(config);
msg_out msg_out
} }
@ -1215,14 +1212,6 @@ where
return (video_sender, audio_sender); return (video_sender, audio_sender);
} }
pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) {
if !t.from_client {
let mut msg_out = Message::new();
msg_out.set_test_delay(t);
allow_err!(peer.send(&msg_out).await);
}
}
// mask = buttons << 3 | type // mask = buttons << 3 | type
// type, 1: down, 2: up, 3: wheel // type, 1: down, 2: up, 3: wheel
// buttons, 1: left, 2: right, 4: middle // buttons, 1: left, 2: right, 4: middle

View File

@ -436,7 +436,12 @@ impl Interface for Session {
let mut displays = Vec::new(); let mut displays = Vec::new();
let mut current = pi.current_display as usize; let mut current = pi.current_display as usize;
if !lc.is_file_transfer { if lc.is_file_transfer {
if pi.username.is_empty() {
self.msgbox("error", "Error", "No active console user logged on, please connect and logon first.");
return;
}
} else {
if pi.displays.is_empty() { if pi.displays.is_empty() {
self.msgbox("error", "Remote Error", "No Display"); self.msgbox("error", "Remote Error", "No Display");
} }

View File

@ -3,6 +3,7 @@ use super::{input_service::*, *};
use crate::clipboard_file::*; use crate::clipboard_file::*;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::common::update_clipboard; use crate::common::update_clipboard;
use crate::video_service;
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
use crate::{common::MOBILE_INFO2, mobile::connection_manager::start_channel}; use crate::{common::MOBILE_INFO2, mobile::connection_manager::start_channel};
use crate::{ipc, VERSION}; use crate::{ipc, VERSION};
@ -69,7 +70,6 @@ pub struct Connection {
audio: bool, audio: bool,
file: bool, file: bool,
last_test_delay: i64, last_test_delay: i64,
image_quality: i32,
lock_after_session_end: bool, lock_after_session_end: bool,
show_remote_cursor: bool, // by peer show_remote_cursor: bool, // by peer
ip: String, ip: String,
@ -105,7 +105,7 @@ impl Subscriber for ConnInner {
} }
} }
const TEST_DELAY_TIMEOUT: Duration = Duration::from_secs(3); const TEST_DELAY_TIMEOUT: Duration = Duration::from_secs(1);
const SEC30: Duration = Duration::from_secs(30); const SEC30: Duration = Duration::from_secs(30);
const H1: Duration = Duration::from_secs(3600); const H1: Duration = Duration::from_secs(3600);
const MILLI1: Duration = Duration::from_millis(1); const MILLI1: Duration = Duration::from_millis(1);
@ -154,7 +154,6 @@ impl Connection {
audio: Config::get_option("enable-audio").is_empty(), audio: Config::get_option("enable-audio").is_empty(),
file: Config::get_option("enable-file-transfer").is_empty(), file: Config::get_option("enable-file-transfer").is_empty(),
last_test_delay: 0, last_test_delay: 0,
image_quality: ImageQuality::Balanced.value(),
lock_after_session_end: false, lock_after_session_end: false,
show_remote_cursor: false, show_remote_cursor: false,
ip: "".to_owned(), ip: "".to_owned(),
@ -376,8 +375,11 @@ impl Connection {
if time > 0 && conn.last_test_delay == 0 { if time > 0 && conn.last_test_delay == 0 {
conn.last_test_delay = time; conn.last_test_delay = time;
let mut msg_out = Message::new(); let mut msg_out = Message::new();
let qos = video_service::VIDEO_QOS.lock().unwrap();
msg_out.set_test_delay(TestDelay{ msg_out.set_test_delay(TestDelay{
time, time,
last_delay:qos.current_delay,
target_bitrate:qos.target_bitrate,
..Default::default() ..Default::default()
}); });
conn.inner.send(msg_out.into()); conn.inner.send(msg_out.into());
@ -394,9 +396,8 @@ impl Connection {
let _ = privacy_mode::turn_off_privacy(0); let _ = privacy_mode::turn_off_privacy(0);
} }
video_service::notify_video_frame_feched(id, None); video_service::notify_video_frame_feched(id, None);
video_service::update_test_latency(id, 0);
video_service::update_image_quality(id, None);
scrap::codec::Encoder::update_video_encoder(id, scrap::codec::EncoderUpdate::Remove); scrap::codec::Encoder::update_video_encoder(id, scrap::codec::EncoderUpdate::Remove);
video_service::VIDEO_QOS.lock().unwrap().reset();
if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await { if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await {
conn.on_close(&err.to_string(), false); conn.on_close(&err.to_string(), false);
} }
@ -665,7 +666,7 @@ impl Connection {
res.set_peer_info(pi); res.set_peer_info(pi);
} else { } else {
try_activate_screen(); try_activate_screen();
match super::video_service::get_displays() { match video_service::get_displays() {
Err(err) => { Err(err) => {
res.set_error(format!("X11 error: {}", err)); res.set_error(format!("X11 error: {}", err));
} }
@ -903,10 +904,11 @@ impl Connection {
self.inner.send(msg_out.into()); self.inner.send(msg_out.into());
} else { } else {
self.last_test_delay = 0; self.last_test_delay = 0;
let latency = crate::get_time() - t.time; let new_delay = (crate::get_time() - t.time) as u32;
if latency > 0 { video_service::VIDEO_QOS
super::video_service::update_test_latency(self.inner.id(), latency); .lock()
} .unwrap()
.update_network_delay(new_delay);
} }
} else if self.authorized { } else if self.authorized {
match msg.union { match msg.union {
@ -1082,7 +1084,7 @@ impl Connection {
}, },
Some(message::Union::misc(misc)) => match misc.union { Some(message::Union::misc(misc)) => match misc.union {
Some(misc::Union::switch_display(s)) => { Some(misc::Union::switch_display(s)) => {
super::video_service::switch_display(s.display); video_service::switch_display(s.display);
} }
Some(misc::Union::chat_message(c)) => { Some(misc::Union::chat_message(c)) => {
self.send_to_cm(ipc::Data::ChatMessage { text: c.text }); self.send_to_cm(ipc::Data::ChatMessage { text: c.text });
@ -1092,7 +1094,7 @@ impl Connection {
} }
Some(misc::Union::refresh_video(r)) => { Some(misc::Union::refresh_video(r)) => {
if r { if r {
super::video_service::refresh(); video_service::refresh();
} }
} }
Some(misc::Union::video_received(_)) => { Some(misc::Union::video_received(_)) => {
@ -1112,13 +1114,18 @@ impl Connection {
async fn update_option(&mut self, o: &OptionMessage) { async fn update_option(&mut self, o: &OptionMessage) {
log::info!("Option update: {:?}", o); log::info!("Option update: {:?}", o);
if let Ok(q) = o.image_quality.enum_value() { if let Ok(q) = o.image_quality.enum_value() {
self.image_quality = q.value(); let mut image_quality = None;
super::video_service::update_image_quality(self.inner.id(), Some(q.value())); if let ImageQuality::NotSet = q {
} if o.custom_image_quality > 0 {
let q = o.custom_image_quality; image_quality = Some(o.custom_image_quality);
if q > 0 { }
self.image_quality = q; } else {
super::video_service::update_image_quality(self.inner.id(), Some(q)); image_quality = Some(q.value() as _)
}
video_service::VIDEO_QOS
.lock()
.unwrap()
.update_image_quality(image_quality);
} }
if let Ok(q) = o.lock_after_session_end.enum_value() { if let Ok(q) = o.lock_after_session_end.enum_value() {
if q != BoolOption::NotSet { if q != BoolOption::NotSet {

View File

@ -37,19 +37,249 @@ use std::{
use virtual_display; use virtual_display;
pub const NAME: &'static str = "video"; pub const NAME: &'static str = "video";
const FPS: u8 = 30;
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref CURRENT_DISPLAY: Arc<Mutex<usize>> = Arc::new(Mutex::new(usize::MAX)); static ref CURRENT_DISPLAY: Arc<Mutex<usize>> = Arc::new(Mutex::new(usize::MAX));
static ref LAST_ACTIVE: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now())); static ref LAST_ACTIVE: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
static ref SWITCH: Arc<Mutex<bool>> = Default::default(); static ref SWITCH: Arc<Mutex<bool>> = Default::default();
static ref TEST_LATENCIES: Arc<Mutex<HashMap<i32, i64>>> = Default::default();
static ref IMAGE_QUALITIES: Arc<Mutex<HashMap<i32, i32>>> = Default::default();
static ref FRAME_FETCHED_NOTIFIER: (UnboundedSender<(i32, Option<Instant>)>, Arc<TokioMutex<UnboundedReceiver<(i32, Option<Instant>)>>>) = { static ref FRAME_FETCHED_NOTIFIER: (UnboundedSender<(i32, Option<Instant>)>, Arc<TokioMutex<UnboundedReceiver<(i32, Option<Instant>)>>>) = {
let (tx, rx) = unbounded_channel(); let (tx, rx) = unbounded_channel();
(tx, Arc::new(TokioMutex::new(rx))) (tx, Arc::new(TokioMutex::new(rx)))
}; };
static ref PRIVACY_MODE_CONN_ID: Mutex<i32> = Mutex::new(0); static ref PRIVACY_MODE_CONN_ID: Mutex<i32> = Mutex::new(0);
static ref IS_CAPTURER_MAGNIFIER_SUPPORTED: bool = is_capturer_mag_supported(); static ref IS_CAPTURER_MAGNIFIER_SUPPORTED: bool = is_capturer_mag_supported();
pub static ref VIDEO_QOS: Arc<Mutex<VideoQoS>> = Default::default();
}
pub struct VideoQoS {
width: u32,
height: u32,
user_image_quality: u32,
current_image_quality: u32,
enable_abr: bool,
pub current_delay: u32,
pub fps: u8, // abr
pub target_bitrate: u32, // abr
updated: bool,
state: AdaptiveState,
last_delay: u32,
count: u32,
}
#[derive(Debug)]
enum AdaptiveState {
Normal,
LowDelay,
HeightDelay,
}
impl Default for VideoQoS {
fn default() -> Self {
VideoQoS {
fps: FPS,
user_image_quality: ImageQuality::Balanced.value() as _,
current_image_quality: ImageQuality::Balanced.value() as _,
enable_abr: false,
width: 0,
height: 0,
current_delay: 0,
target_bitrate: 0,
updated: false,
state: AdaptiveState::Normal,
last_delay: 0,
count: 0,
}
}
}
const MAX: f32 = 1.2;
const MIN: f32 = 0.8;
const MAX_COUNT: u32 = 3;
const MAX_DELAY: u32 = 500;
const MIN_DELAY: u32 = 50;
impl VideoQoS {
pub fn set_size(&mut self, width: u32, height: u32) {
if width == 0 || height == 0 {
return;
}
self.width = width;
self.height = height;
}
pub fn spf(&mut self) -> Duration {
if self.fps <= 0 {
self.fps = FPS;
}
time::Duration::from_secs_f32(1. / (self.fps as f32))
}
// abr
pub fn update_network_delay(&mut self, delay: u32) {
if self.current_delay.eq(&0) {
self.current_delay = delay;
return;
}
let current_delay = self.current_delay as f32;
self.current_delay = delay / 2 + self.current_delay / 2;
log::debug!(
"update_network_delay:{}, {}, state:{:?},count:{}",
self.current_delay,
delay,
self.state,
self.count
);
if self.current_delay < MIN_DELAY {
if self.fps != 30 && self.current_image_quality != self.user_image_quality {
log::debug!("current_delay is normal, set to user_image_quality");
self.fps = 30;
self.current_image_quality = self.user_image_quality;
let _ = self.generate_bitrate().ok();
self.updated = true;
}
self.state = AdaptiveState::Normal;
} else if self.current_delay > MAX_DELAY {
if self.fps != 5 && self.current_image_quality != 25 {
log::debug!("current_delay is very height, set fps to 5, image_quality to 25");
self.fps = 5;
self.current_image_quality = 25;
let _ = self.generate_bitrate().ok();
self.updated = true;
}
} else {
let delay = delay as f32;
let last_delay = self.last_delay as f32;
match self.state {
AdaptiveState::Normal => {
if delay > current_delay * MAX {
self.state = AdaptiveState::HeightDelay;
} else if delay < current_delay * MIN
&& self.current_image_quality < self.user_image_quality
{
self.state = AdaptiveState::LowDelay;
}
self.count = 1;
self.last_delay = self.current_delay
}
AdaptiveState::HeightDelay => {
if delay > last_delay {
if self.count > MAX_COUNT {
self.decrease_quality();
self.reset_state();
return;
}
self.count += 1;
} else {
self.reset_state();
}
}
AdaptiveState::LowDelay => {
if delay < last_delay * MIN {
if self.count > MAX_COUNT {
self.increase_quality();
self.reset_state();
return;
}
self.count += 1;
} else {
self.reset_state();
}
}
}
}
}
fn reset_state(&mut self) {
self.count = 0;
self.state = AdaptiveState::Normal;
}
fn increase_quality(&mut self) {
log::debug!("Adaptive increase quality");
if self.fps < FPS {
log::debug!("increase fps {} -> {}", self.fps, FPS);
self.fps = FPS;
} else {
self.current_image_quality += self.current_image_quality / 2;
let _ = self.generate_bitrate().ok();
log::debug!("increase quality:{}", self.current_image_quality);
}
self.updated = true;
}
fn decrease_quality(&mut self) {
log::debug!("Adaptive decrease quality");
if self.fps < 15 {
log::debug!("fps is low enough :{}", self.fps);
return;
}
if self.current_image_quality < ImageQuality::Low.value() as _ {
self.fps = self.fps / 2;
log::debug!("decrease fps:{}", self.fps);
} else {
self.current_image_quality -= self.current_image_quality / 2;
let _ = self.generate_bitrate().ok();
log::debug!("decrease quality:{}", self.current_image_quality);
};
self.updated = true;
}
pub fn update_image_quality(&mut self, image_quality: Option<u32>) {
if let Some(image_quality) = image_quality {
if image_quality < 10 || image_quality > 200 {
self.current_image_quality = ImageQuality::Balanced.value() as _;
}
if self.current_image_quality != image_quality {
self.current_image_quality = image_quality;
let _ = self.generate_bitrate().ok();
self.updated = true;
}
} else {
self.current_image_quality = ImageQuality::Balanced.value() as _;
}
self.user_image_quality = self.current_image_quality;
}
pub fn generate_bitrate(&mut self) -> ResultType<u32> {
// https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/
if self.width == 0 || self.height == 0 {
bail!("Fail to generate_bitrate, width or height is not set");
}
if self.current_image_quality == 0 {
self.current_image_quality = ImageQuality::Balanced.value() as _;
}
let base_bitrate = ((self.width * self.height) / 800) as u32;
#[cfg(target_os = "android")]
{
// fix when andorid screen shrinks
let fix = Display::fix_quality() as u32;
log::debug!("Android screen, fix quality:{}", fix);
let base_bitrate = base_bitrate * fix;
self.target_bitrate = base_bitrate * self.image_quality / 100;
Ok(self.target_bitrate)
}
self.target_bitrate = base_bitrate * self.current_image_quality / 100;
Ok(self.target_bitrate)
}
pub fn check_if_updated(&mut self) -> bool {
if self.updated {
self.updated = false;
return true;
}
return false;
}
pub fn reset(&mut self) {
*self = Default::default();
}
} }
fn is_capturer_mag_supported() -> bool { fn is_capturer_mag_supported() -> bool {
@ -129,7 +359,7 @@ impl VideoFrameController {
} }
trait TraitCapturer { trait TraitCapturer {
fn frame<'a>(&'a mut self, timeout_ms: u32) -> Result<Frame<'a>>; fn frame<'a>(&'a mut self, timeout: Duration) -> Result<Frame<'a>>;
#[cfg(windows)] #[cfg(windows)]
fn is_gdi(&self) -> bool; fn is_gdi(&self) -> bool;
@ -138,8 +368,8 @@ trait TraitCapturer {
} }
impl TraitCapturer for Capturer { impl TraitCapturer for Capturer {
fn frame<'a>(&'a mut self, timeout_ms: u32) -> Result<Frame<'a>> { fn frame<'a>(&'a mut self, timeout: Duration) -> Result<Frame<'a>> {
self.frame(timeout_ms) self.frame(timeout)
} }
#[cfg(windows)] #[cfg(windows)]
@ -326,9 +556,6 @@ fn run(sp: GenericService) -> ResultType<()> {
#[cfg(windows)] #[cfg(windows)]
ensure_close_virtual_device()?; ensure_close_virtual_device()?;
let fps = 30;
let wait = 1000 / fps;
let spf = time::Duration::from_secs_f32(1. / (fps as f32));
let (ndisplay, current, display) = get_current_display()?; let (ndisplay, current, display) = get_current_display()?;
let (origin, width, height) = (display.origin(), display.width(), display.height()); let (origin, width, height) = (display.origin(), display.width(), display.height());
log::debug!( log::debug!(
@ -342,16 +569,21 @@ fn run(sp: GenericService) -> ResultType<()> {
num_cpus::get(), num_cpus::get(),
); );
let q = get_image_quality(); let mut video_qos = VIDEO_QOS.lock().unwrap();
let (bitrate, rc_min_quantizer, rc_max_quantizer, speed) = get_quality(width, height, q);
log::info!("bitrate={}, rc_min_quantizer={}", bitrate, rc_min_quantizer); video_qos.set_size(width as _, height as _);
let mut spf = video_qos.spf();
let bitrate = video_qos.generate_bitrate()?;
drop(video_qos);
log::info!("init bitrate={}", bitrate);
let encoder_cfg = match Encoder::current_hw_encoder_name() { let encoder_cfg = match Encoder::current_hw_encoder_name() {
Some(codec_name) => EncoderCfg::HW(HwEncoderConfig { Some(codec_name) => EncoderCfg::HW(HwEncoderConfig {
codec_name, codec_name,
width, width,
height, height,
bitrate_ratio: q >> 8, bitrate_ratio: bitrate as _,
}), }),
None => EncoderCfg::VPX(VpxEncoderConfig { None => EncoderCfg::VPX(VpxEncoderConfig {
width: width as _, width: width as _,
@ -359,9 +591,6 @@ fn run(sp: GenericService) -> ResultType<()> {
timebase: [1, 1000], // Output timestamp precision timebase: [1, 1000], // Output timestamp precision
bitrate, bitrate,
codec: VpxVideoCodecId::VP9, codec: VpxVideoCodecId::VP9,
rc_min_quantizer,
rc_max_quantizer,
speed,
num_threads: (num_cpus::get() / 2) as _, num_threads: (num_cpus::get() / 2) as _,
}), }),
}; };
@ -418,10 +647,24 @@ fn run(sp: GenericService) -> ResultType<()> {
let mut try_gdi = 1; let mut try_gdi = 1;
#[cfg(windows)] #[cfg(windows)]
log::info!("gdi: {}", c.is_gdi()); log::info!("gdi: {}", c.is_gdi());
while sp.ok() { while sp.ok() {
#[cfg(windows)] #[cfg(windows)]
check_uac_switch(privacy_mode_id, captuerer_privacy_mode_id)?; check_uac_switch(privacy_mode_id, captuerer_privacy_mode_id)?;
{
let mut video_qos = VIDEO_QOS.lock().unwrap();
if video_qos.check_if_updated() {
log::debug!(
"qos is updated, target_bitrate:{}, fps:{}",
video_qos.target_bitrate,
video_qos.fps
);
encoder.set_bitrate(video_qos.target_bitrate).unwrap();
spf = video_qos.spf();
}
}
if *SWITCH.lock().unwrap() { if *SWITCH.lock().unwrap() {
bail!("SWITCH"); bail!("SWITCH");
} }
@ -430,9 +673,6 @@ fn run(sp: GenericService) -> ResultType<()> {
bail!("SWITCH"); bail!("SWITCH");
} }
check_privacy_mode_changed(&sp, privacy_mode_id)?; check_privacy_mode_changed(&sp, privacy_mode_id)?;
if get_image_quality() != q {
bail!("SWITCH");
}
#[cfg(windows)] #[cfg(windows)]
{ {
if crate::platform::windows::desktop_changed() { if crate::platform::windows::desktop_changed() {
@ -454,7 +694,7 @@ fn run(sp: GenericService) -> ResultType<()> {
frame_controller.reset(); frame_controller.reset();
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
let res = match (*c).frame(wait as _) { let res = match c.frame(spf) {
Ok(frame) => { Ok(frame) => {
let time = now - start; let time = now - start;
let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64;
@ -477,7 +717,7 @@ fn run(sp: GenericService) -> ResultType<()> {
}; };
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
let res = match (*c).frame(wait as _) { let res = match c.frame(spf) {
Ok(frame) => { Ok(frame) => {
let time = now - start; let time = now - start;
let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64;
@ -743,82 +983,3 @@ fn get_current_display() -> ResultType<(usize, usize, Display)> {
} }
return Ok((n, current, displays.remove(current))); return Ok((n, current, displays.remove(current)));
} }
#[inline]
fn update_latency(id: i32, latency: i64, latencies: &mut HashMap<i32, i64>) {
if latency <= 0 {
latencies.remove(&id);
} else {
latencies.insert(id, latency);
}
}
pub fn update_test_latency(id: i32, latency: i64) {
update_latency(id, latency, &mut *TEST_LATENCIES.lock().unwrap());
}
fn convert_quality(q: i32) -> i32 {
let q = {
if q == ImageQuality::Balanced.value() {
(100 * 2 / 3, 12)
} else if q == ImageQuality::Low.value() {
(100 / 2, 18)
} else if q == ImageQuality::Best.value() {
(100, 12)
} else {
let bitrate = q >> 8 & 0xFF;
let quantizer = q & 0xFF;
(bitrate * 2, (100 - quantizer) * 36 / 100)
}
};
if q.0 <= 0 {
0
} else {
q.0 << 8 | q.1
}
}
pub fn update_image_quality(id: i32, q: Option<i32>) {
match q {
Some(q) => {
let q = convert_quality(q);
if q > 0 {
IMAGE_QUALITIES.lock().unwrap().insert(id, q);
} else {
IMAGE_QUALITIES.lock().unwrap().remove(&id);
}
}
None => {
IMAGE_QUALITIES.lock().unwrap().remove(&id);
}
}
}
fn get_image_quality() -> i32 {
IMAGE_QUALITIES
.lock()
.unwrap()
.values()
.min()
.unwrap_or(&convert_quality(ImageQuality::Balanced.value()))
.clone()
}
#[inline]
fn get_quality(w: usize, h: usize, q: i32) -> (u32, u32, u32, i32) {
// https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/
let bitrate = q >> 8 & 0xFF;
let quantizer = q & 0xFF;
let b = ((w * h) / 1000) as u32;
#[cfg(target_os = "android")]
{
// fix when andorid screen shrinks
let fix = Display::fix_quality() as u32;
log::debug!("Android screen, fix quality:{}", fix);
let b = b * fix;
return (bitrate as u32 * b / 100, quantizer as _, 56, 7);
}
(bitrate as u32 * b / 100, quantizer as _, 56, 7)
}

View File

@ -159,6 +159,7 @@ class Header: Reactor.Component {
<li #custom type="image-quality"><span>{svg_checkmark}</span>{translate('Custom')}</li> <li #custom type="image-quality"><span>{svg_checkmark}</span>{translate('Custom')}</li>
<div .separator /> <div .separator />
<li #show-remote-cursor .toggle-option><span>{svg_checkmark}</span>{translate('Show remote cursor')}</li> <li #show-remote-cursor .toggle-option><span>{svg_checkmark}</span>{translate('Show remote cursor')}</li>
<li #show-quality-monitor .toggle-option><span>{svg_checkmark}</span>{translate('Show quality monitor')}</li>
{audio_enabled ? <li #disable-audio .toggle-option><span>{svg_checkmark}</span>{translate('Mute')}</li> : ""} {audio_enabled ? <li #disable-audio .toggle-option><span>{svg_checkmark}</span>{translate('Mute')}</li> : ""}
{is_win && pi.platform == 'Windows' && file_enabled ? <li #enable-file-transfer .toggle-option><span>{svg_checkmark}</span>{translate('Allow file copy and paste')}</li> : ""} {is_win && pi.platform == 'Windows' && file_enabled ? <li #enable-file-transfer .toggle-option><span>{svg_checkmark}</span>{translate('Allow file copy and paste')}</li> : ""}
{keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>{translate('Disable clipboard')}</li> : ""} {keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>{translate('Disable clipboard')}</li> : ""}
@ -315,7 +316,9 @@ class Header: Reactor.Component {
handle_custom_image_quality(); handle_custom_image_quality();
} else if (me.id == "privacy-mode") { } else if (me.id == "privacy-mode") {
togglePrivacyMode(me.id); togglePrivacyMode(me.id);
} else if (me.attributes.hasClass("toggle-option")) { } else if (me.id == "show-quality-monitor") {
toggleQualityMonitor(me.id);
}else if (me.attributes.hasClass("toggle-option")) {
handler.toggle_option(me.id); handler.toggle_option(me.id);
toggleMenuState(); toggleMenuState();
} else if (!me.attributes.hasClass("selected")) { } else if (!me.attributes.hasClass("selected")) {
@ -332,16 +335,13 @@ class Header: Reactor.Component {
} }
function handle_custom_image_quality() { function handle_custom_image_quality() {
var tmp = handler.get_custom_image_quality(); var bitrate = handler.get_custom_image_quality()[0] / 2;
var bitrate0 = tmp[0] || 50;
var quantizer0 = tmp.length > 1 ? tmp[1] : 100;
msgbox("custom", "Custom Image Quality", "<div .form> \ msgbox("custom", "Custom Image Quality", "<div .form> \
<div><input type=\"hslider\" style=\"width: 50%\" name=\"bitrate\" max=\"100\" min=\"10\" value=\"" + bitrate0 + "\"/ buddy=\"bitrate-buddy\"><b #bitrate-buddy>x</b>% bitrate</div> \ <div><input type=\"hslider\" style=\"width: 50%\" name=\"bitrate\" max=\"100\" min=\"10\" value=\"" + bitrate + "\"/ buddy=\"bitrate-buddy\"><b #bitrate-buddy>x</b>% Bitrate</div> \
<div><input type=\"hslider\" style=\"width: 50%\" name=\"quantizer\" max=\"100\" min=\"0\" value=\"" + quantizer0 + "\"/ buddy=\"quantizer-buddy\"><b #quantizer-buddy>x</b>% quantizer</div> \
</div>", function(res=null) { </div>", function(res=null) {
if (!res) return; if (!res) return;
if (!res.bitrate) return; if (!res.bitrate) return;
handler.save_custom_image_quality(res.bitrate, res.quantizer); handler.save_custom_image_quality(res.bitrate * 2);
toggleMenuState(); toggleMenuState();
}); });
} }
@ -357,7 +357,7 @@ function toggleMenuState() {
for (var el in $$(menu#display-options>li)) { for (var el in $$(menu#display-options>li)) {
el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0);
} }
for (var id in ["show-remote-cursor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end"]) { for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end"]) {
var el = self.select('#' + id); var el = self.select('#' + id);
if (el) { if (el) {
var value = handler.get_toggle_option(id); var value = handler.get_toggle_option(id);
@ -425,6 +425,17 @@ function togglePrivacyMode(privacy_id) {
} }
} }
function toggleQualityMonitor(name) {
var show = handler.get_toggle_option(name);
if (show) {
$(#quality-monitor).style.set{ display: "none" };
} else {
$(#quality-monitor).style.set{ display: "block" };
}
handler.toggle_option(name);
toggleMenuState();
}
handler.updateBlockInputState = function(input_blocked) { handler.updateBlockInputState = function(input_blocked) {
if (!input_blocked) { if (!input_blocked) {
handler.toggle_option("block-input"); handler.toggle_option("block-input");

View File

@ -9,6 +9,16 @@ div#video-wrapper {
background: #212121; background: #212121;
} }
div#quality-monitor {
top: 20px;
right: 20px;
background: #7571719c;
padding: 5px;
min-width: 150px;
color: azure;
border: solid azure;
}
video#handler { video#handler {
behavior: native-remote video; behavior: native-remote video;
size: *; size: *;
@ -24,7 +34,7 @@ img#cursor {
} }
.goup { .goup {
transform: rotate(90deg); transform: rotate(90deg);
} }
table#remote-folder-view { table#remote-folder-view {
@ -33,4 +43,4 @@ table#remote-folder-view {
table#local-folder-view { table#local-folder-view {
context-menu: selector(menu#local-folder-view); context-menu: selector(menu#local-folder-view);
} }

View File

@ -1,12 +1,13 @@
<html window-resizable window-frame="extended"> <html window-resizable window-frame="extended">
<head>
<style> <head>
@import url(common.css); <style>
@import url(remote.css); @import url(common.css);
@import url(file_transfer.css); @import url(remote.css);
@import url(header.css); @import url(file_transfer.css);
</style> @import url(header.css);
<script type="text/tiscript"> </style>
<script type="text/tiscript">
include "common.tis"; include "common.tis";
include "msgbox.tis"; include "msgbox.tis";
include "remote.tis"; include "remote.tis";
@ -15,23 +16,28 @@
include "grid.tis"; include "grid.tis";
include "header.tis"; include "header.tis";
</script> </script>
</head> </head>
<header> <header>
<div.window-icon role="window-icon"><icon /></div> <div.window-icon role="window-icon">
<icon />
</div>
<caption role="window-caption" /> <caption role="window-caption" />
<div.window-toolbar /> <div.window-toolbar />
<div.window-buttons /> <div.window-buttons />
</header> </header>
<body>
<div #video-wrapper> <body>
<video #handler> <div #video-wrapper>
<div style="position: relative"> <video #handler>
<img #cursor src="in-memory:cursor" /> <div #quality-monitor style="position: absolute; display: none" />
</div> <div style="position: relative">
</video> <img #cursor src="in-memory:cursor" />
</div> </div>
<div #file-transfer-wrapper> </video>
</div> </div>
<div #msgbox /> <div #file-transfer-wrapper>
</body> </div>
</html> <div #msgbox />
</body>
</html>

View File

@ -2,7 +2,7 @@ use std::{
collections::HashMap, collections::HashMap,
ops::Deref, ops::Deref,
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, AtomicUsize, Ordering},
Arc, Mutex, RwLock, Arc, Mutex, RwLock,
}, },
}; };
@ -223,7 +223,7 @@ impl sciter::EventHandler for Handler {
fn get_custom_image_quality(); fn get_custom_image_quality();
fn save_view_style(String); fn save_view_style(String);
fn save_image_quality(String); fn save_image_quality(String);
fn save_custom_image_quality(i32, i32); fn save_custom_image_quality(i32);
fn refresh_video(); fn refresh_video();
fn get_toggle_option(String); fn get_toggle_option(String);
fn is_privacy_mode_supported(); fn is_privacy_mode_supported();
@ -234,6 +234,25 @@ impl sciter::EventHandler for Handler {
} }
} }
#[derive(Debug)]
struct QualityStatus {
speed: String,
fps: i32,
delay: i32,
target_bitrate: i32,
}
impl Default for QualityStatus {
fn default() -> Self {
Self {
speed: Default::default(),
fps: -1,
delay: -1,
target_bitrate: -1,
}
}
}
impl Handler { impl Handler {
pub fn new(cmd: String, id: String, args: Vec<String>) -> Self { pub fn new(cmd: String, id: String, args: Vec<String>) -> Self {
let me = Self { let me = Self {
@ -249,6 +268,18 @@ impl Handler {
me me
} }
fn update_quality_status(&self, status: QualityStatus) {
self.call2(
"updateQualityStatus",
&make_args!(
status.speed,
status.fps,
status.delay,
status.target_bitrate
),
);
}
fn start_keyboard_hook(&self) { fn start_keyboard_hook(&self) {
if self.is_port_forward() || self.is_file_transfer() { if self.is_port_forward() || self.is_file_transfer() {
return; return;
@ -533,12 +564,12 @@ impl Handler {
self.send(Data::Message(LoginConfigHandler::refresh())); self.send(Data::Message(LoginConfigHandler::refresh()));
} }
fn save_custom_image_quality(&mut self, bitrate: i32, quantizer: i32) { fn save_custom_image_quality(&mut self, custom_image_quality: i32) {
let msg = self let msg = self
.lc .lc
.write() .write()
.unwrap() .unwrap()
.save_custom_image_quality(bitrate, quantizer); .save_custom_image_quality(custom_image_quality as u32);
self.send(Data::Message(msg)); self.send(Data::Message(msg));
} }
@ -1296,7 +1327,10 @@ async fn io_loop(handler: Handler) {
} }
return; return;
} }
let (video_sender, audio_sender) = start_video_audio_threads(|data: &[u8]| { let frame_count = Arc::new(AtomicUsize::new(0));
let frame_count_cl = frame_count.clone();
let (video_sender, audio_sender) = start_video_audio_threads(move |data: &[u8]| {
frame_count_cl.fetch_add(1, Ordering::Relaxed);
VIDEO VIDEO
.lock() .lock()
.unwrap() .unwrap()
@ -1319,6 +1353,8 @@ async fn io_loop(handler: Handler) {
first_frame: false, first_frame: false,
#[cfg(windows)] #[cfg(windows)]
clipboard_file_context: None, clipboard_file_context: None,
data_count: Arc::new(AtomicUsize::new(0)),
frame_count,
}; };
remote.io_loop(&key, &token).await; remote.io_loop(&key, &token).await;
remote.sync_jobs_status_to_local().await; remote.sync_jobs_status_to_local().await;
@ -1369,6 +1405,8 @@ struct Remote {
first_frame: bool, first_frame: bool,
#[cfg(windows)] #[cfg(windows)]
clipboard_file_context: Option<Box<CliprdrClientContext>>, clipboard_file_context: Option<Box<CliprdrClientContext>>,
data_count: Arc<AtomicUsize>,
frame_count: Arc<AtomicUsize>,
} }
impl Remote { impl Remote {
@ -1394,6 +1432,8 @@ impl Remote {
#[cfg(windows)] #[cfg(windows)]
let mut rx_clip_client = get_rx_clip_client().lock().await; let mut rx_clip_client = get_rx_clip_client().lock().await;
let mut status_timer = time::interval(Duration::new(1, 0));
loop { loop {
tokio::select! { tokio::select! {
res = peer.next() => { res = peer.next() => {
@ -1406,6 +1446,7 @@ impl Remote {
} }
Ok(ref bytes) => { Ok(ref bytes) => {
last_recv_time = Instant::now(); last_recv_time = Instant::now();
self.data_count.fetch_add(bytes.len(), Ordering::Relaxed);
if !self.handle_msg_from_peer(bytes, &mut peer).await { if !self.handle_msg_from_peer(bytes, &mut peer).await {
break break
} }
@ -1450,6 +1491,16 @@ impl Remote {
self.timer = time::interval_at(Instant::now() + SEC30, SEC30); self.timer = time::interval_at(Instant::now() + SEC30, SEC30);
} }
} }
_ = status_timer.tick() => {
let speed = self.data_count.swap(0, Ordering::Relaxed);
let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32);
let fps = self.frame_count.swap(0, Ordering::Relaxed) as _;
self.handler.update_quality_status(QualityStatus {
speed,
fps,
..Default::default()
});
}
} }
} }
log::debug!("Exit io_loop of id={}", self.handler.id); log::debug!("Exit io_loop of id={}", self.handler.id);
@ -2370,7 +2421,7 @@ impl Remote {
} }
back_notification::PrivacyModeState::OffSucceeded => { back_notification::PrivacyModeState::OffSucceeded => {
self.handler self.handler
.msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode");
self.update_privacy_mode(false); self.update_privacy_mode(false);
} }
back_notification::PrivacyModeState::OffByPeer => { back_notification::PrivacyModeState::OffByPeer => {
@ -2549,7 +2600,16 @@ impl Interface for Handler {
} }
async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) {
handle_test_delay(t, peer).await; if !t.from_client {
self.update_quality_status(QualityStatus {
delay: t.last_delay as _,
target_bitrate: t.target_bitrate as _,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_test_delay(t);
allow_err!(peer.send(&msg_out).await);
}
} }
} }

View File

@ -456,6 +456,45 @@ function self.closing() {
if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h); if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h);
} }
var qualityMonitor;
var qualityMonitorData = [];
class QualityMonitor: Reactor.Component
{
function this() {
qualityMonitor = this;
if (handler.get_toggle_option("show-quality-monitor")) {
$(#quality-monitor).style.set{ display: "block" };
}
}
function render() {
return <div >
<div>
Speed: {qualityMonitorData[0]}
</div>
<div>
FPS: {qualityMonitorData[1]}
</div>
<div>
Delay: {qualityMonitorData[2]} ms
</div>
<div>
Target Bitrate: {qualityMonitorData[3]}kb
</div>
</div>;
}
}
$(#quality-monitor).content(<QualityMonitor />);
handler.updateQualityStatus = function(speed, fps, delay, bitrate) {
speed ? qualityMonitorData[0] = speed:null;
fps > -1 ? qualityMonitorData[1] = fps:null;
delay > -1 ? qualityMonitorData[2] = delay:null;
bitrate > -1 ? qualityMonitorData[3] = bitrate:null;
qualityMonitor.update();
}
handler.setPermission = function(name, enabled) { handler.setPermission = function(name, enabled) {
self.timer(60ms, function() { self.timer(60ms, function() {
if (name == "keyboard") keyboard_enabled = enabled; if (name == "keyboard") keyboard_enabled = enabled;