mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-16 17:31:04 +03:00
Compare commits
41 Commits
webrtc-ipv
...
webrtc-ans
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
731df99b0c | ||
|
|
092a961b62 | ||
|
|
4f56d6957a | ||
|
|
f312b57909 | ||
|
|
f9c2f16857 | ||
|
|
8154c026a4 | ||
|
|
28269af9c7 | ||
|
|
20e25e743d | ||
|
|
851d2df88c | ||
|
|
be757de462 | ||
|
|
a437eb9bc6 | ||
|
|
39d4f1854b | ||
|
|
bd028ad350 | ||
|
|
b48fe17b52 | ||
|
|
82b6fc2f3b | ||
|
|
29772f6595 | ||
|
|
d0ee56d349 | ||
|
|
4515cd6463 | ||
|
|
9b5f342839 | ||
|
|
cdcb4d4b4c | ||
|
|
34c340be4e | ||
|
|
9f60bb6fc5 | ||
|
|
5d7e20103a | ||
|
|
e4b9594c5e | ||
|
|
8b716c7046 | ||
|
|
70d45051f6 | ||
|
|
bf1ebe5be2 | ||
|
|
3c7c13d79d | ||
|
|
e54f21e10c | ||
|
|
e82dd12350 | ||
|
|
d5311574be | ||
|
|
0843447c41 | ||
|
|
67e0a1f582 | ||
|
|
91c9fccbb0 | ||
|
|
c4221469d8 | ||
|
|
978e2e28b9 | ||
|
|
5cfe136fb0 | ||
|
|
14a5ed45d9 | ||
|
|
435fe24a81 | ||
|
|
3ffee7c1ff | ||
|
|
97190f715b |
30
.github/workflows/flutter-build.yml
vendored
30
.github/workflows/flutter-build.yml
vendored
@@ -374,6 +374,12 @@ jobs:
|
||||
|
||||
- name: Add MSBuild to PATH
|
||||
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
|
||||
with:
|
||||
# Select the MSBuild process architecture; -p:Platform sets the MSI target.
|
||||
# Native ARM64 tools give the compiler more address space for PCH files
|
||||
# (C3859/C1076 were reported by HostX86\arm64\CL.exe).
|
||||
# Keep the action's default x86 MSBuild for the existing x64 MSI job.
|
||||
msbuild-architecture: ${{ matrix.job.arch == 'aarch64' && 'arm64' || 'x86' }}
|
||||
|
||||
- name: Build msi
|
||||
# Builds the MSI for the matrix arch. res/msi (WiX v4 + native CustomActions) carries
|
||||
@@ -492,11 +498,20 @@ jobs:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: nightly-2023-10-13-${{ matrix.job.target }} # must use nightly here, because of abi_thiscall feature required
|
||||
targets: ${{ matrix.job.target }}
|
||||
components: "rustfmt"
|
||||
shell: bash
|
||||
run: |
|
||||
# Sciter's abi_thiscall feature requires nightly Rust.
|
||||
# Use an i686 host toolchain so build scripts can load the 32-bit LLVM installed above.
|
||||
# Since rustup 1.29.1, i686 toolchains on x64 Windows require --force-non-host
|
||||
# for both installation and default selection, even though WOW64 can run them.
|
||||
# See https://github.com/rust-lang/rustup/pull/4935.
|
||||
# Alternatively, an x64 compiler could use cargo build --target i686-pc-windows-msvc.
|
||||
# This requires 64-bit LLVM for host build scripts and updated packaging paths,
|
||||
# and has not been manually verified for RustDesk.
|
||||
rustup toolchain install nightly-2023-10-13-${{ matrix.job.target }} \
|
||||
--target ${{ matrix.job.target }} --component rustfmt \
|
||||
--profile minimal --no-self-update --force-non-host
|
||||
rustup default nightly-2023-10-13-${{ matrix.job.target }} --force-non-host
|
||||
|
||||
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
with:
|
||||
@@ -916,8 +931,6 @@ jobs:
|
||||
|
||||
- name: Codesign app and create signed dmg
|
||||
if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true'
|
||||
env:
|
||||
MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }}
|
||||
run: |
|
||||
# Patch create-dmg to give more attempts to unmount image
|
||||
CREATE_DMG="$(command -v create-dmg)"
|
||||
@@ -928,9 +941,10 @@ jobs:
|
||||
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
|
||||
# start sign the rustdesk.app and dmg
|
||||
rm -rf *.dmg || true
|
||||
# the identity secret carries its own shell quoting, so expand it inline like the dmg codesign below
|
||||
bash ./.github/scripts/sign-macos-app.sh \
|
||||
./flutter/build/macos/Build/Products/Release/RustDesk.app \
|
||||
"$MACOS_CODESIGN_IDENTITY" \
|
||||
${{ secrets.MACOS_CODESIGN_IDENTITY }} \
|
||||
./flutter/macos/Runner/Release.entitlements
|
||||
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
|
||||
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
|
||||
|
||||
58
Cargo.lock
generated
58
Cargo.lock
generated
@@ -985,9 +985,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
dependencies = [
|
||||
"serde 1.0.228",
|
||||
]
|
||||
@@ -1717,7 +1717,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.15.3"
|
||||
source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#69ad2578adc9200093fc81cdfbdad63dbc4274f9"
|
||||
source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#96d4da121b7d949677ac5b6887413a9185fd7f39"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"cidre",
|
||||
@@ -1791,9 +1791,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
version = "0.9.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
@@ -4121,8 +4121,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "interceptor"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ac0781c825d602095113772e389ef0607afcb869ae0e68a590d8e0799cdcef8"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
@@ -6496,9 +6495,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.13"
|
||||
version = "0.11.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||
checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.2",
|
||||
@@ -7008,8 +7007,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rtcp"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9689528bf3a9eb311fd938d05516dd546412f9ce4fffc8acfc1db27cc3dbf72"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"thiserror 1.0.61",
|
||||
@@ -7019,8 +7017,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rtp"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c54733451a67d76caf9caa07a7a2cec6871ea9dda92a7847f98063d459200f4b"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
@@ -7139,6 +7136,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"libpulse-binding",
|
||||
"libpulse-simple-binding",
|
||||
"libsamplerate-sys",
|
||||
"libxdo-sys",
|
||||
"mac_address",
|
||||
"magnum-opus",
|
||||
@@ -7464,8 +7462,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sdp"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd277015eada44a0bb810a4b84d3bf6e810573fa62fb442f457edf6a1087a69"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"rand 0.8.5",
|
||||
"substring",
|
||||
@@ -8037,8 +8034,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "stun"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dbc2bab375524093c143dc362a03fb6a1fb79e938391cdb21665688f88a088a"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"crc",
|
||||
@@ -8920,8 +8916,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "turn"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f5aea1116456e1da71c45586b87c72e3b43164fbf435eb93ff6aa475416a9a4"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -9584,8 +9579,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24bab7195998d605c862772f90a452ba655b90a2f463c850ac032038890e367a"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
@@ -9628,8 +9622,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-data"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e97b932854da633a767eff0cc805425a2222fc6481e96f463e57b015d949d1d"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"log",
|
||||
@@ -9643,8 +9636,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-dtls"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ccbe4d9049390ab52695c3646c1395c877e16c15fb05d3bda8eee0c7351711c"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
@@ -9680,8 +9672,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-ice"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb51bde0d790f109a15bfe4d04f1b56fb51d567da231643cb3f21bb74d678997"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
@@ -9705,8 +9696,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-mdns"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "979cc85259c53b7b620803509d10d35e2546fa505d228850cbe3f08765ea6ea8"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"log",
|
||||
"socket2 0.5.10",
|
||||
@@ -9718,8 +9708,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-media"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80041211deccda758a3e19aa93d6b10bc1d37c9183b519054b40a83691d13810"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
@@ -9731,7 +9720,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-sctp"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=db3b07a9dd8f195916c89c2e62a8911402b11d27#db3b07a9dd8f195916c89c2e62a8911402b11d27"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
@@ -9748,8 +9737,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-srtp"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01e773f79b09b057ffbda6b03fe7b43403b012a240cf8d05d630674c3723b5bb"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
@@ -9771,7 +9759,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-util"
|
||||
version = "0.11.0"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=db3b07a9dd8f195916c89c2e62a8911402b11d27#db3b07a9dd8f195916c89c2e62a8911402b11d27"
|
||||
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bitflags 1.3.2",
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -22,7 +22,7 @@ path = "src/service.rs"
|
||||
|
||||
[features]
|
||||
inline = []
|
||||
use_samplerate = ["samplerate"]
|
||||
use_samplerate = ["samplerate", "libsamplerate-sys"]
|
||||
use_rubato = ["rubato"]
|
||||
use_dasp = ["dasp"]
|
||||
flutter = ["flutter_rust_bridge"]
|
||||
@@ -67,6 +67,7 @@ magnum-opus = { git = "https://github.com/rustdesk-org/magnum-opus" }
|
||||
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
|
||||
rubato = { version = "0.12", optional = true }
|
||||
samplerate = { version = "0.2", optional = true }
|
||||
libsamplerate-sys = { version = "0.1.12", optional = true }
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
num_cpus = "1.15"
|
||||
bytes = { version = "1.4", features = ["serde"] }
|
||||
@@ -230,8 +231,11 @@ libxdo-sys = { path = "libs/libxdo-sys-stub" }
|
||||
# the SACK settle the rest (F-RTO), timed from the latest send, so a stall no longer resends the
|
||||
# whole backlog behind itself while a short lost tail still comes back at once.
|
||||
# Pinned by rev, not branch: a fork branch can be rewritten out from under the lockfile.
|
||||
webrtc-util = { git = "https://github.com/rustdesk-org/webrtc", rev = "db3b07a9dd8f195916c89c2e62a8911402b11d27" }
|
||||
webrtc-sctp = { git = "https://github.com/rustdesk-org/webrtc", rev = "db3b07a9dd8f195916c89c2e62a8911402b11d27" }
|
||||
# webrtc: SettingEngine cannot reach the ICE agent's max_binding_requests, which decides how
|
||||
# long the answerer keeps checking a pair that has not answered yet.
|
||||
webrtc = { git = "https://github.com/rustdesk-org/webrtc", rev = "80d5a20532cf58f5d4d237c437a98ceb85ee40dc" }
|
||||
webrtc-util = { git = "https://github.com/rustdesk-org/webrtc", rev = "80d5a20532cf58f5d4d237c437a98ceb85ee40dc" }
|
||||
webrtc-sctp = { git = "https://github.com/rustdesk-org/webrtc", rev = "80d5a20532cf58f5d4d237c437a98ceb85ee40dc" }
|
||||
|
||||
[package.metadata.winres]
|
||||
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:bullseye-slim
|
||||
FROM debian:trixie-slim
|
||||
|
||||
WORKDIR /
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
11
fastlane/metadata/android/es-ES/full_description.txt
Normal file
11
fastlane/metadata/android/es-ES/full_description.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Aplicación de escritorio remoto de código abierto, la alternativa open source a TeamViewer.
|
||||
Código fuente: https://github.com/rustdesk/rustdesk
|
||||
Documentación: https://rustdesk.com/docs/en/manual/mobile/
|
||||
|
||||
Para que un dispositivo remoto controle tu Android mediante el ratón o el tacto, debes permitir que RustDesk utilice el servicio de "Accesibilidad". RustDesk utiliza la API AccessibilityService para implementar el control remoto en Android.
|
||||
|
||||
Además del control remoto, también puedes transferir archivos fácilmente entre dispositivos Android y ordenadores mediante RustDesk.
|
||||
|
||||
Tienes control total de tus datos, sin preocupaciones de seguridad. Puedes utilizar nuestro servidor rendezvous/relay, optar por el autoalojamiento o escribir tu propio servidor rendezvous/relay. El servidor autoalojado es gratuito y de código abierto: https://github.com/rustdesk/rustdesk-server
|
||||
|
||||
Descarga e instala la versión de escritorio desde: https://rustdesk.com — entonces podrás acceder y controlar tu ordenador desde tu teléfono, o controlar tu teléfono desde tu ordenador.
|
||||
1
fastlane/metadata/android/es-ES/short_description.txt
Normal file
1
fastlane/metadata/android/es-ES/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Aplicación de acceso remoto de código abierto, alternativa a TeamViewer.
|
||||
11
fastlane/metadata/android/it-IT/full_description.txt
Normal file
11
fastlane/metadata/android/it-IT/full_description.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Applicazione open source per desktop remoto, l'alternativa open source a TeamViewer.
|
||||
Codice sorgente: https://github.com/rustdesk/rustdesk
|
||||
Documentazione: https://rustdesk.com/docs/it/client/android/
|
||||
|
||||
Per consentire a un dispositivo remoto di controllare il tuo Android tramite mouse o tocco, devi permettere a RustDesk di utilizzare il servizio "Accessibilità". RustDesk utilizza l'API AccessibilityService per implementare il controllo remoto su Android.
|
||||
|
||||
Oltre al controllo remoto, puoi anche trasferire facilmente file tra dispositivi Android e PC con RustDesk.
|
||||
|
||||
Hai il controllo totale dei tuoi dati, senza preoccupazioni per la sicurezza. Puoi usare il nostro server rendezvous/relay, optare per l'hosting autonomo, o scrivere il tuo server rendezvous/relay. Il server auto-ospitato è gratuito e open source: https://github.com/rustdesk/rustdesk-server
|
||||
|
||||
Scarica e installa la versione per desktop da: https://rustdesk.com — potrai quindi accedere e controllare il tuo computer dal cellulare, o controllare il tuo cellulare dal computer.
|
||||
1
fastlane/metadata/android/it-IT/short_description.txt
Normal file
1
fastlane/metadata/android/it-IT/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
App open source per desktop remoto, alternativa a TeamViewer.
|
||||
11
fastlane/metadata/android/pt-BR/full_description.txt
Normal file
11
fastlane/metadata/android/pt-BR/full_description.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Aplicativo de desktop remoto de código aberto, a alternativa open source ao TeamViewer.
|
||||
Código-fonte: https://github.com/rustdesk/rustdesk
|
||||
Documentação: https://rustdesk.com/docs/pt/client/android/
|
||||
|
||||
Para que um dispositivo remoto controle seu Android via mouse ou toque, você precisa permitir que o RustDesk utilize o serviço de "Acessibilidade". O RustDesk usa a API AccessibilityService para implementar o controle remoto no Android.
|
||||
|
||||
Além do controle remoto, você também pode transferir arquivos entre dispositivos Android e PCs com facilidade usando o RustDesk.
|
||||
|
||||
Você tem controle total dos seus dados, sem preocupações com a segurança. Você pode usar nosso servidor rendezvous/relay, optar pela auto-hospedagem ou criar seu próprio servidor de rendezvous/relay. O servidor auto-hospedado é gratuito e open source: https://github.com/rustdesk/rustdesk-server
|
||||
|
||||
Baixe e instale a versão para desktop em: https://rustdesk.com — então você poderá acessar e controlar seu computador pelo celular ou controlar seu celular pelo computador.
|
||||
1
fastlane/metadata/android/pt-BR/short_description.txt
Normal file
1
fastlane/metadata/android/pt-BR/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Aplicativo de acesso remoto open source, alternativa ao TeamViewer.
|
||||
@@ -51,6 +51,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
||||
private var minBufferSize = 0
|
||||
private var audioRecordStat = false
|
||||
private var audioThread: Thread? = null
|
||||
private var playbackCapturePending = false
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun createAudioRecorder(inVoiceCall: Boolean, mediaProjection: MediaProjection?): Boolean {
|
||||
@@ -193,6 +194,17 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
||||
return audioRecorder?.audioSource == MediaRecorder.AudioSource.VOICE_COMMUNICATION
|
||||
}
|
||||
|
||||
fun getVoiceCallStartError(): String {
|
||||
return if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
) != PackageManager.PERMISSION_GRANTED) {
|
||||
"To start a voice call, enable \"Audio capture\" on the \"Screen share\" page."
|
||||
} else {
|
||||
"Failed to start voice call."
|
||||
}
|
||||
}
|
||||
|
||||
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
||||
if (!isSupportVoiceCall()) {
|
||||
return false
|
||||
@@ -220,6 +232,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
||||
if (it.getAudioSource() == MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
|
||||
return true
|
||||
}
|
||||
playbackCapturePending = true
|
||||
}
|
||||
audioRecordStat = false
|
||||
audioThread?.join()
|
||||
@@ -234,11 +247,10 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun switchOutVoiceCall(mediaProjection: MediaProjection?): Boolean {
|
||||
audioRecorder?.let {
|
||||
if (it.getAudioSource() != MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
|
||||
return true
|
||||
}
|
||||
if (!isVoiceCallActive() && !playbackCapturePending) {
|
||||
return true
|
||||
}
|
||||
playbackCapturePending = true
|
||||
audioRecordStat = false
|
||||
audioThread?.join()
|
||||
|
||||
@@ -246,7 +258,11 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
||||
Log.e(logTag, "createAudioRecorder fail")
|
||||
return false
|
||||
}
|
||||
return startAudioRecorder()
|
||||
val started = startAudioRecorder()
|
||||
if (started) {
|
||||
playbackCapturePending = false
|
||||
}
|
||||
return started
|
||||
}
|
||||
|
||||
fun tryReleaseAudio() {
|
||||
@@ -256,6 +272,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
||||
audioRecordStat = false
|
||||
audioThread?.join()
|
||||
audioThread = null
|
||||
playbackCapturePending = false
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
|
||||
@@ -969,7 +969,7 @@ class MainActivity : FlutterActivity() {
|
||||
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to "Failed to start voice call."))
|
||||
"text" to audioRecordHandle.getVoiceCallStartError()))
|
||||
} else {
|
||||
Log.d(logTag, "onVoiceCallStarted success")
|
||||
}
|
||||
|
||||
@@ -153,19 +153,13 @@ class MainService : Service() {
|
||||
} else {
|
||||
if (!switchOutVoiceCall()) {
|
||||
Log.e(logTag, "switchOutVoiceCall fail")
|
||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to "Failed to switch out voice call."))
|
||||
showVoiceCallError("Failed to switch out voice call.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!switchToVoiceCall()) {
|
||||
Log.e(logTag, "switchToVoiceCall fail")
|
||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to "Failed to switch to voice call."))
|
||||
showVoiceCallError(audioRecordHandle.getVoiceCallStartError())
|
||||
}
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
@@ -507,6 +501,15 @@ class MainService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showVoiceCallError(message: String) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to message))
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun startMicrophoneCapture(startAudio: () -> Boolean): Boolean {
|
||||
if (!setMicrophoneForegroundService(true)) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.carriez.flutter_hbb
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.media.projection.MediaProjectionConfig
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -19,7 +20,13 @@ class PermissionRequestTransparentActivity: Activity() {
|
||||
ACT_REQUEST_MEDIA_PROJECTION -> {
|
||||
val mediaProjectionManager =
|
||||
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
val intent = mediaProjectionManager.createScreenCaptureIntent()
|
||||
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
mediaProjectionManager.createScreenCaptureIntent(
|
||||
MediaProjectionConfig.createConfigForDefaultDisplay()
|
||||
)
|
||||
} else {
|
||||
mediaProjectionManager.createScreenCaptureIntent()
|
||||
}
|
||||
startActivityForResult(intent, REQ_REQUEST_MEDIA_PROJECTION)
|
||||
}
|
||||
else -> finish()
|
||||
|
||||
@@ -178,6 +178,7 @@ const String kOptionEnableIpv6Punch = "enable-ipv6-punch";
|
||||
const String kOptionAllowSyncClipboardBetweenSessions =
|
||||
"allow-sync-clipboard-between-sessions";
|
||||
const String kOptionEnableWebrtc = "enable-webrtc";
|
||||
const String kOptionRelayFallbackDelay = "relay-fallback-delay";
|
||||
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||
const String kOptionShowVirtualMouse = "show-virtual-mouse";
|
||||
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
||||
|
||||
@@ -591,13 +591,7 @@ class _GeneralState extends State<_General> {
|
||||
isServer: false,
|
||||
),
|
||||
],
|
||||
if (!incomingOnly)
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable WebRTC P2P connection',
|
||||
kOptionEnableWebrtc,
|
||||
isServer: false,
|
||||
),
|
||||
if (!incomingOnly) ...webrtcOptions(context),
|
||||
if (!isWeb && !incomingOnly)
|
||||
Tooltip(
|
||||
message: translate('sync-clipboard-between-sessions-tip'),
|
||||
@@ -887,6 +881,85 @@ class _GeneralState extends State<_General> {
|
||||
).marginOnly(left: _kContentHMargin);
|
||||
});
|
||||
}
|
||||
|
||||
// How long an already-connected relay is held back to give the direct WebRTC
|
||||
// attempt a chance to win. It only means anything while WebRTC is on, so it
|
||||
// follows the checkbox as an indented sub-option and is hidden outright when
|
||||
// the box is clear — the shape `directIp` uses for its port.
|
||||
List<Widget> webrtcOptions(BuildContext context) {
|
||||
final stored = bind.mainGetLocalOption(key: kOptionRelayFallbackDelay);
|
||||
final controller = TextEditingController(text: stored);
|
||||
// What the field holds against what is saved. Apply is offered only while
|
||||
// the two differ, so an untouched field shows no button at all, and neither
|
||||
// does one typed back to its saved value or cleared when nothing was saved
|
||||
// — the state an "edited" flag alone would still call dirty.
|
||||
final typed = RxString(stored);
|
||||
final saved = RxString(stored);
|
||||
return [
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable WebRTC P2P connection',
|
||||
kOptionEnableWebrtc,
|
||||
isServer: false,
|
||||
update: (_) => setState(() {}),
|
||||
),
|
||||
() {
|
||||
final enabled = mainGetLocalBoolOptionSync(kOptionEnableWebrtc);
|
||||
final isOptFixed = isOptionFixed(kOptionRelayFallbackDelay);
|
||||
return Offstage(
|
||||
offstage: !enabled,
|
||||
child: Tooltip(
|
||||
message: translate('relay-fallback-delay-tip'),
|
||||
child: _SubLabeledWidget(
|
||||
context,
|
||||
'Relay fallback delay in seconds',
|
||||
Row(children: [
|
||||
SizedBox(
|
||||
width: 95,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
enabled: enabled && !isOptFixed,
|
||||
onChanged: (v) => typed.value = v,
|
||||
inputFormatters: [
|
||||
// Seconds, at most one decimal. Clearing the field is
|
||||
// allowed and restores the built-in default.
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'^([0-9]|[1-9][0-9])(\.[0-9]?)?$')),
|
||||
],
|
||||
decoration: const InputDecoration(
|
||||
hintText: '2.5',
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
),
|
||||
).workaroundFreezeLinuxMint().marginOnly(right: 15),
|
||||
),
|
||||
Obx(() => Offstage(
|
||||
offstage: typed.value.trim() == saved.value.trim(),
|
||||
child: ElevatedButton(
|
||||
onPressed: enabled &&
|
||||
!isOptFixed &&
|
||||
!typed.value.trim().endsWith('.') &&
|
||||
double.tryParse(typed.value.trim()) != 0
|
||||
? () async {
|
||||
final v = controller.text.trim();
|
||||
await bind.mainSetLocalOption(
|
||||
key: kOptionRelayFallbackDelay, value: v);
|
||||
if (controller.text != v) controller.text = v;
|
||||
typed.value = v;
|
||||
saved.value = v;
|
||||
}
|
||||
: null,
|
||||
child: Text(translate('Apply')),
|
||||
),
|
||||
))
|
||||
]),
|
||||
enabled: enabled && !isOptFixed,
|
||||
),
|
||||
),
|
||||
);
|
||||
}(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
enum _AccessMode {
|
||||
|
||||
@@ -175,7 +175,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
// `on_voice_call_closed` should be called when the connection is ended.
|
||||
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
|
||||
// Only one client is considered here for now.
|
||||
gFFI.chatModel.onVoiceCallClosed("End connetion");
|
||||
gFFI.chatModel.onVoiceCallClosed("End connection");
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -141,7 +141,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
// `on_voice_call_closed` should be called when the connection is ended.
|
||||
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
|
||||
// Only one client is considered here for now.
|
||||
gFFI.chatModel.onVoiceCallClosed("End connetion");
|
||||
gFFI.chatModel.onVoiceCallClosed("End connection");
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -3601,9 +3601,11 @@ class QualityMonitorModel with ChangeNotifier {
|
||||
bool get show => _show;
|
||||
QualityMonitorData get data => _data;
|
||||
|
||||
// Only a WebRTC session names its transport here: web has no session tab
|
||||
// to show it on, and WebRTC is the one path that can be direct or TURN.
|
||||
// Only a WebRTC session on the web names its transport here: web has no
|
||||
// session tab to show it on (the desktop tab's tooltip already does), and
|
||||
// WebRTC is the one path that can be direct or TURN.
|
||||
String? get webrtcTransport {
|
||||
if (!isWeb) return null;
|
||||
final ffiModel = parent.target?.ffiModel;
|
||||
if (ffiModel == null) return null;
|
||||
final streamType = ffiModel.cachedPeerData.streamType;
|
||||
|
||||
28
flutter/test/quality_monitor_transport_test.dart
Normal file
28
flutter/test/quality_monitor_transport_test.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
final _sessionId = UuidValue('00000000-0000-0000-0000-000000000000');
|
||||
|
||||
class _FakeFFI implements FFI {
|
||||
@override
|
||||
UuidValue get sessionId => _sessionId;
|
||||
|
||||
@override
|
||||
late final FfiModel ffiModel = FfiModel(WeakReference(this));
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('the quality monitor names the WebRTC transport only on web', () {
|
||||
final ffi = _FakeFFI();
|
||||
ffi.ffiModel.cachedPeerData.streamType = 'WebRTC';
|
||||
final model = QualityMonitorModel(WeakReference(ffi));
|
||||
// Off the web the session tab's tooltip already names the transport.
|
||||
expect(isWeb, isFalse);
|
||||
expect(model.webrtcTransport, isNull);
|
||||
});
|
||||
}
|
||||
@@ -129,6 +129,7 @@ pub const OPTION_ENABLE_UDP_PUNCH: &str = "enable-udp-punch";
|
||||
pub const OPTION_ENABLE_IPV6_PUNCH: &str = "enable-ipv6-punch";
|
||||
pub const OPTION_ENABLE_PORT_FORWARD_MUX: &str = "enable-port-forward-mux";
|
||||
pub const OPTION_ENABLE_WEBRTC: &str = "enable-webrtc";
|
||||
pub const OPTION_RELAY_FALLBACK_DELAY: &str = "relay-fallback-delay";
|
||||
pub const OPTION_ALLOW_KCP_CC: &str = "allow-kcp-congestion-control";
|
||||
pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card";
|
||||
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
|
||||
@@ -258,6 +259,7 @@ pub const KEYS_LOCAL_SETTINGS: &[&str] = &[
|
||||
OPTION_ENABLE_IPV6_PUNCH,
|
||||
OPTION_ENABLE_PORT_FORWARD_MUX,
|
||||
OPTION_ENABLE_WEBRTC,
|
||||
OPTION_RELAY_FALLBACK_DELAY,
|
||||
OPTION_TOUCH_MODE,
|
||||
OPTION_SHOW_VIRTUAL_MOUSE,
|
||||
OPTION_SHOW_VIRTUAL_JOYSTICK,
|
||||
|
||||
Submodule libs/hbb_common updated: 29cf7cbe4d...daf84c2df3
@@ -41,6 +41,13 @@ impl<T> Drop for ComPtr<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum FrameState {
|
||||
Idle,
|
||||
Acquired,
|
||||
Mapped,
|
||||
}
|
||||
|
||||
pub struct Capturer {
|
||||
device: ComPtr<ID3D11Device>,
|
||||
display: Display,
|
||||
@@ -58,6 +65,7 @@ pub struct Capturer {
|
||||
output_texture: bool,
|
||||
adapter_desc1: DXGI_ADAPTER_DESC1,
|
||||
rotate: Rotate,
|
||||
frame_state: FrameState,
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
@@ -174,6 +182,7 @@ impl Capturer {
|
||||
output_texture: false,
|
||||
adapter_desc1,
|
||||
rotate,
|
||||
frame_state: FrameState::Idle,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -335,6 +344,7 @@ impl Capturer {
|
||||
let mut info = mem::MaybeUninit::uninit().assume_init();
|
||||
|
||||
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
|
||||
self.frame_state = FrameState::Acquired;
|
||||
let frame = ComPtr(frame);
|
||||
|
||||
if *info.LastPresentTime.QuadPart() == 0 {
|
||||
@@ -345,9 +355,11 @@ impl Capturer {
|
||||
let mut rect = mem::MaybeUninit::uninit().assume_init();
|
||||
if self.fastlane {
|
||||
wrap_hresult((*self.duplication.0).MapDesktopSurface(&mut rect))?;
|
||||
self.frame_state = FrameState::Mapped;
|
||||
} else {
|
||||
self.surface = ComPtr(self.ohgodwhat(frame.0)?);
|
||||
wrap_hresult((*self.surface.0).Map(&mut rect, DXGI_MAP_READ))?;
|
||||
self.frame_state = FrameState::Mapped;
|
||||
}
|
||||
Ok((rect.pBits, rect.Pitch))
|
||||
}
|
||||
@@ -424,7 +436,7 @@ impl Capturer {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.unmap();
|
||||
self.release_frame()?;
|
||||
let r = self.load_frame(timeout)?;
|
||||
let rotate = match self.display.rotation() {
|
||||
DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => kRotate0,
|
||||
@@ -472,12 +484,13 @@ impl Capturer {
|
||||
if self.duplication.0.is_null() {
|
||||
return Err(std::io::ErrorKind::AddrNotAvailable.into());
|
||||
}
|
||||
(*self.duplication.0).ReleaseFrame();
|
||||
self.release_frame()?;
|
||||
let mut frame = ptr::null_mut();
|
||||
#[allow(invalid_value)]
|
||||
let mut info = mem::MaybeUninit::uninit().assume_init();
|
||||
|
||||
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
|
||||
self.frame_state = FrameState::Acquired;
|
||||
let frame = ComPtr(frame);
|
||||
|
||||
if info.AccumulatedFrames == 0 || *info.LastPresentTime.QuadPart() == 0 {
|
||||
@@ -574,16 +587,42 @@ impl Capturer {
|
||||
}
|
||||
}
|
||||
|
||||
fn unmap(&self) {
|
||||
fn release_frame(&mut self) -> io::Result<()> {
|
||||
if self.duplication.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut first_error = None;
|
||||
// Unmap before ReleaseFrame invalidates the desktop surface; use the same
|
||||
// order for staging surfaces. Cleanup advances Mapped -> Acquired -> Idle,
|
||||
// while Idle is a no-op. Advance state even on errors to avoid retrying
|
||||
// cleanup, but still attempt ReleaseFrame if unmapping fails.
|
||||
unsafe {
|
||||
(*self.duplication.0).ReleaseFrame();
|
||||
if self.fastlane {
|
||||
(*self.duplication.0).UnMapDesktopSurface();
|
||||
} else {
|
||||
if !self.surface.is_null() {
|
||||
(*self.surface.0).Unmap();
|
||||
if self.frame_state == FrameState::Mapped {
|
||||
let result = if self.fastlane {
|
||||
wrap_hresult((*self.duplication.0).UnMapDesktopSurface())
|
||||
} else if !self.surface.is_null() {
|
||||
wrap_hresult((*self.surface.0).Unmap())
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
self.frame_state = FrameState::Acquired;
|
||||
if let Err(err) = result {
|
||||
first_error = Some(err);
|
||||
}
|
||||
}
|
||||
if self.frame_state == FrameState::Acquired {
|
||||
let result = wrap_hresult((*self.duplication.0).ReleaseFrame());
|
||||
self.frame_state = FrameState::Idle;
|
||||
if first_error.is_none() {
|
||||
if let Err(err) = result {
|
||||
first_error = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match first_error {
|
||||
Some(err) => Err(err),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,8 +638,8 @@ impl Capturer {
|
||||
|
||||
impl Drop for Capturer {
|
||||
fn drop(&mut self) {
|
||||
if !self.duplication.is_null() {
|
||||
self.unmap();
|
||||
if let Err(err) = self.release_frame() {
|
||||
eprintln!("DXGI frame cleanup failed: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ if [ "$1" = configure ]; then
|
||||
if [ "systemd" == "$INITSYS" ]; then
|
||||
|
||||
if [ -e /etc/systemd/system/rustdesk.service ]; then
|
||||
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
|
||||
rm -f /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
|
||||
fi
|
||||
mkdir -p /usr/lib/systemd/system/
|
||||
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# FFmpeg 7.1.1 includes the HEVC WPP slice-thread deadlock fix:
|
||||
# https://github.com/FFmpeg/FFmpeg/commit/79c47dfd25f101b6842bbec8c6ffef8d5077c3ae
|
||||
vcpkg_from_github(
|
||||
OUT_SOURCE_PATH SOURCE_PATH
|
||||
REPO ffmpeg/ffmpeg
|
||||
REF "n${VERSION}"
|
||||
SHA512 3b273769ef1a1b63aed0691eef317a760f8c83b1d0e1c232b67bbee26db60b4864aafbc88df0e86d6bebf07185bbd057f33e2d5258fde6d97763b9994cd48b6f
|
||||
SHA512 6b9a5ee501be41d6abc7579a106263b31f787321cbc45dedee97abf992bf8236cdb2394571dd256a74154f4a20018d429ae7e7f0409611ddc4d6f529d924d175
|
||||
HEAD_REF master
|
||||
PATCHES
|
||||
0001-create-lib-libraries.patch
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "ffmpeg",
|
||||
"version": "7.1",
|
||||
"port-version": 1,
|
||||
"version": "7.1.1",
|
||||
"description": [
|
||||
"a library to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created.",
|
||||
"FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure FATE across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations."
|
||||
|
||||
242
src/audio_resampler.rs
Normal file
242
src/audio_resampler.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
use hbb_common::thiserror;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod allocation_tests;
|
||||
|
||||
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
|
||||
mod sinc;
|
||||
|
||||
const INTERPOLATION_MARGIN_FRAMES: usize = 2;
|
||||
const PENDING_PACKET_CAPACITY: usize = 2;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct AudioResamplerConfig {
|
||||
pub input_rate: u32,
|
||||
pub output_rate: u32,
|
||||
pub channels: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub(crate) enum AudioResamplerError {
|
||||
#[error(
|
||||
"invalid audio resampler configuration: input_rate={}, output_rate={}, channels={}",
|
||||
.0.input_rate, .0.output_rate, .0.channels
|
||||
)]
|
||||
InvalidConfig(AudioResamplerConfig),
|
||||
#[error("invalid resampler output frame size: {output_frames}")]
|
||||
InvalidOutputFrameSize { output_frames: usize },
|
||||
#[error("audio resampler input length {samples} is not divisible by channel count {channels}")]
|
||||
IncompleteFrame { samples: usize, channels: usize },
|
||||
#[error("audio resampler output capacity overflow")]
|
||||
CapacityOverflow,
|
||||
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
|
||||
#[error("audio resampler backend failed: {0}")]
|
||||
Backend(String),
|
||||
}
|
||||
|
||||
pub(crate) struct FixedFrameAudioResampler {
|
||||
resampler: AudioResampler,
|
||||
output_samples: usize,
|
||||
pending_samples: Vec<f32>,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
|
||||
// SAFETY: libsamplerate's src_new state owns heap data and has no thread affinity.
|
||||
// This wrapper never exposes or shares that state; processing requires &mut self.
|
||||
unsafe impl Send for FixedFrameAudioResampler {}
|
||||
|
||||
impl FixedFrameAudioResampler {
|
||||
pub(crate) fn new(
|
||||
config: AudioResamplerConfig,
|
||||
output_frames: usize,
|
||||
) -> Result<Self, AudioResamplerError> {
|
||||
if output_frames == 0 {
|
||||
return Err(AudioResamplerError::InvalidOutputFrameSize { output_frames });
|
||||
}
|
||||
let channels = validate_config(config)?;
|
||||
let output_samples = output_frames
|
||||
.checked_mul(channels)
|
||||
.ok_or(AudioResamplerError::CapacityOverflow)?;
|
||||
let input_frames = output_frames
|
||||
.checked_mul(config.input_rate as usize)
|
||||
.ok_or(AudioResamplerError::CapacityOverflow)?
|
||||
.div_ceil(config.output_rate as usize);
|
||||
let capacity = output_samples
|
||||
.checked_mul(PENDING_PACKET_CAPACITY)
|
||||
.and_then(|samples| samples.checked_add(channels * INTERPOLATION_MARGIN_FRAMES))
|
||||
.ok_or(AudioResamplerError::CapacityOverflow)?;
|
||||
let mut resampler = AudioResampler::new(config)?;
|
||||
resampler.reserve_input(input_frames)?;
|
||||
Ok(Self {
|
||||
resampler,
|
||||
output_samples,
|
||||
pending_samples: Vec::with_capacity(capacity),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn process_with(
|
||||
&mut self,
|
||||
input: &[f32],
|
||||
mut on_packet: impl FnMut(&[f32]),
|
||||
) -> Result<(), AudioResamplerError> {
|
||||
self.resampler
|
||||
.process_into(input, &mut self.pending_samples)?;
|
||||
let complete_samples =
|
||||
self.pending_samples.len() / self.output_samples * self.output_samples;
|
||||
for packet in self.pending_samples[..complete_samples].chunks_exact(self.output_samples) {
|
||||
on_packet(packet);
|
||||
}
|
||||
self.pending_samples.drain(..complete_samples);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn process(&mut self, input: &[f32]) -> Result<Vec<Vec<f32>>, AudioResamplerError> {
|
||||
let mut packets = Vec::new();
|
||||
self.process_with(input, |packet| packets.push(packet.to_owned()))?;
|
||||
Ok(packets)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AudioResampler {
|
||||
#[cfg(not(all(feature = "use_samplerate", not(feature = "use_dasp"))))]
|
||||
backend: StreamingLinearAudioResampler,
|
||||
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
|
||||
backend: sinc::SincAudioResampler,
|
||||
}
|
||||
|
||||
impl AudioResampler {
|
||||
pub(crate) fn new(config: AudioResamplerConfig) -> Result<Self, AudioResamplerError> {
|
||||
Ok(Self {
|
||||
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
|
||||
backend: sinc::SincAudioResampler::new(config)?,
|
||||
#[cfg(not(all(feature = "use_samplerate", not(feature = "use_dasp"))))]
|
||||
backend: StreamingLinearAudioResampler::new(config)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn process(&mut self, input: &[f32]) -> Result<Vec<f32>, AudioResamplerError> {
|
||||
let mut output = Vec::new();
|
||||
self.process_into(input, &mut output)?;
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
// Append samples so capture can retain an incomplete output packet in the same buffer.
|
||||
fn process_into(
|
||||
&mut self,
|
||||
input: &[f32],
|
||||
output: &mut Vec<f32>,
|
||||
) -> Result<(), AudioResamplerError> {
|
||||
self.backend.process_into(input, output)
|
||||
}
|
||||
|
||||
fn reserve_input(&mut self, _frames: usize) -> Result<(), AudioResamplerError> {
|
||||
#[cfg(not(all(feature = "use_samplerate", not(feature = "use_dasp"))))]
|
||||
{
|
||||
let capacity = _frames
|
||||
.checked_add(INTERPOLATION_MARGIN_FRAMES)
|
||||
.and_then(|frames| frames.checked_mul(self.backend.channels))
|
||||
.ok_or(AudioResamplerError::CapacityOverflow)?;
|
||||
self.backend.buffered_samples.reserve(capacity);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "use_samplerate", not(feature = "use_dasp"))))]
|
||||
struct StreamingLinearAudioResampler {
|
||||
config: AudioResamplerConfig,
|
||||
channels: usize,
|
||||
buffered_samples: Vec<f32>,
|
||||
next_position: u64,
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "use_samplerate", not(feature = "use_dasp"))))]
|
||||
impl StreamingLinearAudioResampler {
|
||||
fn new(config: AudioResamplerConfig) -> Result<Self, AudioResamplerError> {
|
||||
Ok(Self {
|
||||
config,
|
||||
channels: validate_config(config)?,
|
||||
buffered_samples: Vec::new(),
|
||||
next_position: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn process_into(
|
||||
&mut self,
|
||||
input: &[f32],
|
||||
output: &mut Vec<f32>,
|
||||
) -> Result<(), AudioResamplerError> {
|
||||
validate_input(input, self.channels)?;
|
||||
let capacity = self.output_capacity(input.len())?;
|
||||
output.reserve(capacity);
|
||||
self.buffered_samples.extend_from_slice(input);
|
||||
while self.write_next_frame(output) {
|
||||
self.next_position += self.config.input_rate as u64;
|
||||
}
|
||||
self.discard_consumed_frames();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_capacity(&self, input_samples: usize) -> Result<usize, AudioResamplerError> {
|
||||
let input_frames = input_samples / self.channels;
|
||||
let scaled_frames = input_frames
|
||||
.checked_mul(self.config.output_rate as usize)
|
||||
.ok_or(AudioResamplerError::CapacityOverflow)?
|
||||
/ self.config.input_rate as usize;
|
||||
scaled_frames
|
||||
.checked_add(INTERPOLATION_MARGIN_FRAMES)
|
||||
.and_then(|frames| frames.checked_mul(self.channels))
|
||||
.ok_or(AudioResamplerError::CapacityOverflow)
|
||||
}
|
||||
|
||||
fn write_next_frame(&self, output: &mut Vec<f32>) -> bool {
|
||||
let output_rate = self.config.output_rate as u64;
|
||||
let frame_count = self.buffered_samples.len() / self.channels;
|
||||
let frame = (self.next_position / output_rate) as usize;
|
||||
let fraction = self.next_position % output_rate;
|
||||
if frame >= frame_count || (fraction != 0 && frame + 1 >= frame_count) {
|
||||
return false;
|
||||
}
|
||||
let weight = fraction as f32 / output_rate as f32;
|
||||
for channel in 0..self.channels {
|
||||
let current = self.buffered_samples[frame * self.channels + channel];
|
||||
let next_frame = frame + usize::from(fraction != 0);
|
||||
let next = self.buffered_samples[next_frame * self.channels + channel];
|
||||
output.push(current + (next - current) * weight);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn discard_consumed_frames(&mut self) {
|
||||
let output_rate = self.config.output_rate as u64;
|
||||
let available_frames = self.buffered_samples.len() / self.channels;
|
||||
let consumed_frames = ((self.next_position / output_rate) as usize).min(available_frames);
|
||||
self.buffered_samples
|
||||
.drain(0..consumed_frames * self.channels);
|
||||
self.next_position -= consumed_frames as u64 * output_rate;
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_config(config: AudioResamplerConfig) -> Result<usize, AudioResamplerError> {
|
||||
if config.input_rate == 0 || config.output_rate == 0 || config.channels == 0 {
|
||||
return Err(AudioResamplerError::InvalidConfig(config));
|
||||
}
|
||||
Ok(config.channels as usize)
|
||||
}
|
||||
|
||||
fn validate_input(input: &[f32], channels: usize) -> Result<(), AudioResamplerError> {
|
||||
if input.len() % channels != 0 {
|
||||
return Err(AudioResamplerError::IncompleteFrame {
|
||||
samples: input.len(),
|
||||
channels,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(all(feature = "use_samplerate", not(feature = "use_dasp")))))]
|
||||
mod tests;
|
||||
|
||||
#[cfg(all(test, feature = "use_samplerate", not(feature = "use_dasp")))]
|
||||
mod samplerate_tests;
|
||||
142
src/audio_resampler/allocation_tests.rs
Normal file
142
src/audio_resampler/allocation_tests.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use super::{AudioResamplerConfig, FixedFrameAudioResampler};
|
||||
use std::alloc::{GlobalAlloc, Layout, System};
|
||||
use std::cell::Cell;
|
||||
|
||||
struct CountingAllocator;
|
||||
|
||||
thread_local! {
|
||||
static ALLOCATIONS: Cell<Option<usize>> = const { Cell::new(None) };
|
||||
}
|
||||
|
||||
fn record_allocation() {
|
||||
let _ = ALLOCATIONS.try_with(|count| {
|
||||
if let Some(value) = count.get() {
|
||||
count.set(Some(value + 1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unsafe impl GlobalAlloc for CountingAllocator {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
record_allocation();
|
||||
unsafe { System.alloc(layout) }
|
||||
}
|
||||
|
||||
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
|
||||
record_allocation();
|
||||
unsafe { System.alloc_zeroed(layout) }
|
||||
}
|
||||
|
||||
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
|
||||
record_allocation();
|
||||
unsafe { System.realloc(ptr, layout, size) }
|
||||
}
|
||||
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||
unsafe { System.dealloc(ptr, layout) }
|
||||
}
|
||||
}
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: CountingAllocator = CountingAllocator;
|
||||
|
||||
pub(crate) fn assert_no_allocations(process: impl FnOnce()) {
|
||||
struct ResetCounter;
|
||||
impl Drop for ResetCounter {
|
||||
fn drop(&mut self) {
|
||||
ALLOCATIONS.with(|count| count.set(None));
|
||||
}
|
||||
}
|
||||
|
||||
ALLOCATIONS.with(|count| assert!(count.replace(Some(0)).is_none()));
|
||||
let reset = ResetCounter;
|
||||
process();
|
||||
let allocations = ALLOCATIONS.with(|count| count.get().unwrap());
|
||||
drop(reset);
|
||||
assert_eq!(
|
||||
allocations, 0,
|
||||
"PCM processing allocated on the capture thread"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capture_resampling_reuses_buffers() {
|
||||
const PACKETS_PER_SECOND: usize = 100;
|
||||
const PACKET_COUNT: usize = 100;
|
||||
const MAX_STARTUP_DELAY_PACKETS: usize = 1;
|
||||
const SIGNAL_LEVEL: f32 = 0.25;
|
||||
const RATE_PAIRS: [(u32, u32); 6] = [
|
||||
(32_000, 24_000),
|
||||
(44_100, 24_000),
|
||||
(44_100, 48_000),
|
||||
(48_000, 24_000),
|
||||
(96_000, 48_000),
|
||||
(192_000, 48_000),
|
||||
];
|
||||
|
||||
for (input_rate, output_rate) in RATE_PAIRS {
|
||||
for channels in [1, 2, 4, 6, 8] {
|
||||
let config = AudioResamplerConfig {
|
||||
input_rate,
|
||||
output_rate,
|
||||
channels,
|
||||
};
|
||||
let input =
|
||||
vec![SIGNAL_LEVEL; input_rate as usize / PACKETS_PER_SECOND * channels as usize];
|
||||
let frames = output_rate as usize / PACKETS_PER_SECOND;
|
||||
let mut resampler = FixedFrameAudioResampler::new(config, frames).unwrap();
|
||||
let mut packets = 0;
|
||||
let mut energy = 0.0;
|
||||
assert_no_allocations(|| {
|
||||
for _ in 0..PACKET_COUNT {
|
||||
resampler
|
||||
.process_with(&input, |packet| {
|
||||
assert_eq!(packet.len(), frames * channels as usize);
|
||||
energy += packet.iter().map(|sample| sample * sample).sum::<f32>();
|
||||
packets += 1;
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
assert!((PACKET_COUNT - MAX_STARTUP_DELAY_PACKETS..=PACKET_COUNT).contains(&packets));
|
||||
assert!(energy > SIGNAL_LEVEL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
|
||||
#[test]
|
||||
fn sinc_output_matches_the_existing_backend() {
|
||||
use super::AudioResampler;
|
||||
|
||||
const INPUT_FRAMES: usize = 2_048;
|
||||
const CHUNK_FRAMES: usize = 73;
|
||||
const SIGNAL_STEP: f32 = 0.07;
|
||||
for (input_rate, output_rate) in [(44_100, 24_000), (44_100, 48_000), (96_000, 48_000)] {
|
||||
for channels in [1, 2, 4, 6, 8] {
|
||||
let config = AudioResamplerConfig {
|
||||
input_rate,
|
||||
output_rate,
|
||||
channels,
|
||||
};
|
||||
let input: Vec<_> = (0..INPUT_FRAMES * channels as usize)
|
||||
.map(|sample| (sample as f32 * SIGNAL_STEP).sin())
|
||||
.collect();
|
||||
let mut actual = AudioResampler::new(config).unwrap();
|
||||
let expected = samplerate::Samplerate::new(
|
||||
samplerate::ConverterType::SincBestQuality,
|
||||
input_rate,
|
||||
output_rate,
|
||||
channels as usize,
|
||||
)
|
||||
.unwrap();
|
||||
for chunk in input.chunks(CHUNK_FRAMES * channels as usize) {
|
||||
assert_eq!(
|
||||
actual.process(chunk).unwrap(),
|
||||
expected.process(chunk).unwrap()
|
||||
);
|
||||
assert_eq!(actual.process(&[]).unwrap(), expected.process(&[]).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
157
src/audio_resampler/samplerate_tests.rs
Normal file
157
src/audio_resampler/samplerate_tests.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use super::{AudioResampler, AudioResamplerConfig, AudioResamplerError, FixedFrameAudioResampler};
|
||||
|
||||
const INPUT_RATE: u32 = 44_100;
|
||||
const OUTPUT_RATE: u32 = 48_000;
|
||||
const CHANNELS: u16 = 2;
|
||||
const INPUT_PACKET_FRAMES: usize = INPUT_RATE as usize / PACKETS_PER_SECOND;
|
||||
const OUTPUT_PACKET_FRAMES: usize = OUTPUT_RATE as usize / PACKETS_PER_SECOND;
|
||||
const PACKET_COUNT: usize = 20;
|
||||
const PACKETS_PER_SECOND: usize = 100;
|
||||
const MIN_CONTINUITY_PACKETS: usize = 2;
|
||||
const TONE_FREQUENCY_HZ: f32 = 997.0;
|
||||
const TONE_AMPLITUDE: f32 = 0.5;
|
||||
const MAX_BOUNDARY_RESIDUAL: f32 = 0.02;
|
||||
const INCOMPLETE_SAMPLE_COUNT: usize = 1;
|
||||
const DOWNSAMPLE_RATE: u32 = 24_000;
|
||||
const REJECTED_TONE_HZ: f64 = 18_000.0;
|
||||
const MAX_ALIAS_RMS: f64 = 0.01;
|
||||
const MIN_PASSBAND_RMS: f64 = 0.3;
|
||||
|
||||
fn stereo_tone(frames: usize) -> Vec<f32> {
|
||||
(0..frames)
|
||||
.flat_map(|frame| {
|
||||
let phase =
|
||||
std::f32::consts::TAU * TONE_FREQUENCY_HZ * frame as f32 / INPUT_RATE as f32;
|
||||
let sample = TONE_AMPLITUDE * phase.sin();
|
||||
[sample, sample]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn maximum_boundary_residual(packets: &[Vec<f32>]) -> f32 {
|
||||
packets.windows(2).fold(0.0, |maximum, pair| {
|
||||
let previous = &pair[0];
|
||||
let current = &pair[1];
|
||||
let last = previous.len() - CHANNELS as usize;
|
||||
let penultimate = last - CHANNELS as usize;
|
||||
(0..CHANNELS as usize).fold(maximum, |maximum, channel| {
|
||||
let predicted = previous[last + channel]
|
||||
+ (previous[last + channel] - previous[penultimate + channel]);
|
||||
maximum.max((current[channel] - predicted).abs())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn stereo_config() -> AudioResamplerConfig {
|
||||
AudioResamplerConfig {
|
||||
input_rate: INPUT_RATE,
|
||||
output_rate: OUTPUT_RATE,
|
||||
channels: CHANNELS,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_capture_resampler_preserves_pending_audio() {
|
||||
let input = stereo_tone(INPUT_PACKET_FRAMES * PACKET_COUNT);
|
||||
let packet_samples = INPUT_PACKET_FRAMES * CHANNELS as usize;
|
||||
let mut expected_resampler =
|
||||
FixedFrameAudioResampler::new(stereo_config(), OUTPUT_PACKET_FRAMES).unwrap();
|
||||
let expected: Vec<_> = input
|
||||
.chunks(packet_samples)
|
||||
.flat_map(|packet| expected_resampler.process(packet).unwrap())
|
||||
.collect();
|
||||
let mut moved_resampler =
|
||||
FixedFrameAudioResampler::new(stereo_config(), OUTPUT_PACKET_FRAMES).unwrap();
|
||||
let mut output = moved_resampler.process(&input[..packet_samples]).unwrap();
|
||||
let remaining = std::thread::spawn(move || {
|
||||
input[packet_samples..]
|
||||
.chunks(packet_samples)
|
||||
.flat_map(|packet| moved_resampler.process(packet).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
output.extend(remaining);
|
||||
|
||||
assert!(output.len() >= MIN_CONTINUITY_PACKETS);
|
||||
assert!(output
|
||||
.iter()
|
||||
.all(|packet| packet.len() == OUTPUT_PACKET_FRAMES * CHANNELS as usize));
|
||||
assert!(maximum_boundary_residual(&output) <= MAX_BOUNDARY_RESIDUAL);
|
||||
assert_eq!(output, expected);
|
||||
}
|
||||
|
||||
fn downsampled_rms(input: &[f32]) -> f64 {
|
||||
let config = AudioResamplerConfig {
|
||||
output_rate: DOWNSAMPLE_RATE,
|
||||
..stereo_config()
|
||||
};
|
||||
let output_frames = DOWNSAMPLE_RATE as usize / PACKETS_PER_SECOND;
|
||||
let mut resampler = FixedFrameAudioResampler::new(config, output_frames).unwrap();
|
||||
let output: Vec<f32> = input
|
||||
.chunks(INPUT_PACKET_FRAMES * CHANNELS as usize)
|
||||
.flat_map(|packet| resampler.process(packet).unwrap().into_iter().flatten())
|
||||
.collect();
|
||||
|
||||
assert!(output.len() >= output_frames * CHANNELS as usize * MIN_CONTINUITY_PACKETS);
|
||||
let mean_square = output
|
||||
.iter()
|
||||
.map(|sample| f64::from(*sample).powi(2))
|
||||
.sum::<f64>()
|
||||
/ output.len() as f64;
|
||||
mean_square.sqrt()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capture_downsampling_filters_out_of_band_audio() {
|
||||
let input: Vec<_> = (0..INPUT_PACKET_FRAMES * PACKET_COUNT)
|
||||
.flat_map(|frame| {
|
||||
let phase =
|
||||
std::f64::consts::TAU * REJECTED_TONE_HZ * frame as f64 / f64::from(INPUT_RATE);
|
||||
let sample = (f64::from(TONE_AMPLITUDE) * phase.sin()) as f32;
|
||||
[sample, sample]
|
||||
})
|
||||
.collect();
|
||||
let rms = downsampled_rms(&input);
|
||||
assert!(
|
||||
rms < MAX_ALIAS_RMS,
|
||||
"out-of-band output RMS {rms} exceeded {MAX_ALIAS_RMS}"
|
||||
);
|
||||
|
||||
let input = stereo_tone(INPUT_PACKET_FRAMES * PACKET_COUNT);
|
||||
let rms = downsampled_rms(&input);
|
||||
assert!(
|
||||
rms > MIN_PASSBAND_RMS,
|
||||
"in-band output RMS {rms} fell below {MIN_PASSBAND_RMS}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn samplerate_backend_preserves_streaming_continuity() {
|
||||
let input = stereo_tone(INPUT_PACKET_FRAMES * PACKET_COUNT);
|
||||
let mut resampler = AudioResampler::new(stereo_config()).unwrap();
|
||||
let packets: Vec<_> = input
|
||||
.chunks(INPUT_PACKET_FRAMES * CHANNELS as usize)
|
||||
.map(|packet| resampler.process(packet).unwrap())
|
||||
.filter(|packet| !packet.is_empty())
|
||||
.collect();
|
||||
|
||||
assert!(packets.len() >= MIN_CONTINUITY_PACKETS);
|
||||
assert!(packets
|
||||
.iter()
|
||||
.all(|packet| packet.len() % CHANNELS as usize == 0));
|
||||
assert!(maximum_boundary_residual(&packets) <= MAX_BOUNDARY_RESIDUAL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn samplerate_backend_reports_incomplete_frame_context() {
|
||||
let mut resampler = AudioResampler::new(stereo_config()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
resampler.process(&[0.0]).unwrap_err(),
|
||||
AudioResamplerError::IncompleteFrame {
|
||||
samples: INCOMPLETE_SAMPLE_COUNT,
|
||||
channels: CHANNELS as usize,
|
||||
}
|
||||
);
|
||||
}
|
||||
112
src/audio_resampler/sinc.rs
Normal file
112
src/audio_resampler/sinc.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use super::{AudioResamplerConfig, AudioResamplerError};
|
||||
use libsamplerate_sys as sys;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
const OUTPUT_MARGIN_FRAMES: usize = 1;
|
||||
|
||||
pub(super) struct SincAudioResampler {
|
||||
state: NonNull<sys::SRC_STATE>,
|
||||
config: AudioResamplerConfig,
|
||||
}
|
||||
|
||||
impl SincAudioResampler {
|
||||
pub(super) fn new(config: AudioResamplerConfig) -> Result<Self, AudioResamplerError> {
|
||||
super::validate_config(config)?;
|
||||
let ratio = f64::from(config.output_rate) / f64::from(config.input_rate);
|
||||
if unsafe { sys::src_is_valid_ratio(ratio) } == 0 {
|
||||
return Err(backend_error(
|
||||
config,
|
||||
samplerate::ErrorCode::BadSrcRatio as _,
|
||||
));
|
||||
}
|
||||
let mut error = 0;
|
||||
// SAFETY: src_new allocates independent state; this owner releases it in Drop.
|
||||
let state = unsafe {
|
||||
sys::src_new(
|
||||
sys::SRC_SINC_BEST_QUALITY as _,
|
||||
config.channels.into(),
|
||||
&mut error,
|
||||
)
|
||||
};
|
||||
let state = NonNull::new(state).ok_or_else(|| backend_error(config, error))?;
|
||||
Ok(Self { state, config })
|
||||
}
|
||||
|
||||
pub(super) fn process_into(
|
||||
&mut self,
|
||||
input: &[f32],
|
||||
output: &mut Vec<f32>,
|
||||
) -> Result<(), AudioResamplerError> {
|
||||
super::validate_input(input, self.config.channels as usize)?;
|
||||
let mut consumed = 0;
|
||||
loop {
|
||||
let (used, generated) = self.process_block(&input[consumed..], output)?;
|
||||
consumed += used;
|
||||
if consumed == input.len() {
|
||||
return Ok(());
|
||||
}
|
||||
if used == 0 && generated == 0 {
|
||||
return Err(AudioResamplerError::Backend(
|
||||
"libsamplerate made no progress while input remained".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_block(
|
||||
&mut self,
|
||||
input: &[f32],
|
||||
output: &mut Vec<f32>,
|
||||
) -> Result<(usize, usize), AudioResamplerError> {
|
||||
let channels = self.config.channels as usize;
|
||||
let input_frames = input.len() / channels;
|
||||
let output_frames = input_frames
|
||||
.checked_mul(self.config.output_rate as usize)
|
||||
.map(|frames| frames / self.config.input_rate as usize)
|
||||
.and_then(|frames| frames.checked_add(OUTPUT_MARGIN_FRAMES))
|
||||
.ok_or(AudioResamplerError::CapacityOverflow)?;
|
||||
let start = output.len();
|
||||
let end = output_frames
|
||||
.checked_mul(channels)
|
||||
.and_then(|samples| start.checked_add(samples))
|
||||
.ok_or(AudioResamplerError::CapacityOverflow)?;
|
||||
let mut data = sys::SRC_DATA {
|
||||
data_in: input.as_ptr(),
|
||||
input_frames: input_frames
|
||||
.try_into()
|
||||
.map_err(|_| AudioResamplerError::CapacityOverflow)?,
|
||||
output_frames: output_frames
|
||||
.try_into()
|
||||
.map_err(|_| AudioResamplerError::CapacityOverflow)?,
|
||||
src_ratio: f64::from(self.config.output_rate) / f64::from(self.config.input_rate),
|
||||
..Default::default()
|
||||
};
|
||||
output.resize(end, 0.0);
|
||||
data.data_out = output[start..].as_mut_ptr();
|
||||
// SAFETY: state is exclusively owned; disjoint slices cover the declared frame counts.
|
||||
let error = unsafe { sys::src_process(self.state.as_ptr(), &mut data) };
|
||||
let generated = data.output_frames_gen as usize * channels;
|
||||
output.truncate(start + generated);
|
||||
if error != 0 {
|
||||
return Err(backend_error(self.config, error));
|
||||
}
|
||||
Ok((data.input_frames_used as usize * channels, generated))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SincAudioResampler {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: this owner holds the only handle returned by src_new.
|
||||
unsafe { sys::src_delete(self.state.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
fn backend_error(config: AudioResamplerConfig, code: i32) -> AudioResamplerError {
|
||||
AudioResamplerError::Backend(format!(
|
||||
"input_rate={}, output_rate={}, channels={}: {:?}",
|
||||
config.input_rate,
|
||||
config.output_rate,
|
||||
config.channels,
|
||||
samplerate::Error::from_int(code)
|
||||
))
|
||||
}
|
||||
178
src/audio_resampler/tests.rs
Normal file
178
src/audio_resampler/tests.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use super::{AudioResampler, AudioResamplerConfig, FixedFrameAudioResampler};
|
||||
|
||||
const INPUT_RATE: u32 = 24_000;
|
||||
const OUTPUT_RATE: u32 = 48_000;
|
||||
const CHANNELS: u16 = 2;
|
||||
const CHUNK_FRAMES: usize = 240;
|
||||
const CHUNK_COUNT: usize = 4;
|
||||
const TONE_FREQUENCY_HZ: f32 = 997.0;
|
||||
const TONE_AMPLITUDE: f32 = 0.5;
|
||||
const MAX_BOUNDARY_RESIDUAL: f32 = 0.02;
|
||||
const LOOK_AHEAD_OUTPUT_FRAMES: usize = 1;
|
||||
const UNEVEN_CHUNK_FRAMES: usize = 73;
|
||||
const MONO_CHANNELS: u16 = 1;
|
||||
const UNIT_RATE: u32 = 1;
|
||||
const DOUBLE_RATE: u32 = 2;
|
||||
const FIRST_DOWNSAMPLE_PACKET: [f32; 3] = [0.0, 1.0, 2.0];
|
||||
const SECOND_DOWNSAMPLE_PACKET: [f32; 4] = [3.0, 4.0, 5.0, 6.0];
|
||||
const EXPECTED_DOWNSAMPLED_OUTPUT: [f32; 4] = [0.0, 2.0, 4.0, 6.0];
|
||||
const PACKETS_PER_SECOND: usize = 100;
|
||||
const OUTPUT_PACKET_FRAMES: usize = OUTPUT_RATE as usize / PACKETS_PER_SECOND;
|
||||
const RATE_44_1_KHZ: u32 = 44_100;
|
||||
const FLOAT_TOLERANCE: f32 = 0.000_001;
|
||||
const MIN_CONTINUITY_PACKETS: usize = 2;
|
||||
|
||||
fn stereo_tone_at_rate(frames: usize, sample_rate: u32) -> Vec<f32> {
|
||||
(0..frames)
|
||||
.flat_map(|frame| {
|
||||
let phase =
|
||||
std::f32::consts::TAU * TONE_FREQUENCY_HZ * frame as f32 / sample_rate as f32;
|
||||
let sample = TONE_AMPLITUDE * phase.sin();
|
||||
[sample, sample]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn stereo_tone(frames: usize) -> Vec<f32> {
|
||||
stereo_tone_at_rate(frames, INPUT_RATE)
|
||||
}
|
||||
|
||||
fn maximum_tone_prediction_residual(sample_rate: u32) -> f32 {
|
||||
let half_step = std::f32::consts::PI * TONE_FREQUENCY_HZ / sample_rate as f32;
|
||||
4.0 * TONE_AMPLITUDE * half_step.sin().powi(2)
|
||||
}
|
||||
|
||||
fn maximum_boundary_residual(chunks: &[Vec<f32>]) -> f32 {
|
||||
chunks.windows(2).fold(0.0, |maximum, pair| {
|
||||
let previous = &pair[0];
|
||||
let current = &pair[1];
|
||||
let last = previous.len() - CHANNELS as usize;
|
||||
let penultimate = last - CHANNELS as usize;
|
||||
(0..CHANNELS as usize).fold(maximum, |maximum, channel| {
|
||||
let predicted = previous[last + channel]
|
||||
+ (previous[last + channel] - previous[penultimate + channel]);
|
||||
maximum.max((current[channel] - predicted).abs())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn stereo_config() -> AudioResamplerConfig {
|
||||
AudioResamplerConfig {
|
||||
input_rate: INPUT_RATE,
|
||||
output_rate: OUTPUT_RATE,
|
||||
channels: CHANNELS,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_decoded_packet_continuity_and_output_ratio() {
|
||||
let input = stereo_tone(CHUNK_FRAMES * CHUNK_COUNT);
|
||||
let mut whole_resampler = AudioResampler::new(stereo_config()).unwrap();
|
||||
let whole_output = whole_resampler.process(&input).unwrap();
|
||||
let expected_frames = CHUNK_FRAMES * CHUNK_COUNT * OUTPUT_RATE as usize / INPUT_RATE as usize
|
||||
- LOOK_AHEAD_OUTPUT_FRAMES;
|
||||
|
||||
for chunk_frames in [CHUNK_FRAMES, UNEVEN_CHUNK_FRAMES] {
|
||||
let mut resampler = AudioResampler::new(stereo_config()).unwrap();
|
||||
let output: Vec<_> = input
|
||||
.chunks(chunk_frames * CHANNELS as usize)
|
||||
.map(|chunk| resampler.process(chunk).unwrap())
|
||||
.collect();
|
||||
let residual = maximum_boundary_residual(&output);
|
||||
assert!(
|
||||
residual <= MAX_BOUNDARY_RESIDUAL,
|
||||
"packet boundary residual {residual} exceeded {MAX_BOUNDARY_RESIDUAL}, chunk_frames={chunk_frames}"
|
||||
);
|
||||
let output_frames = output.iter().map(Vec::len).sum::<usize>() / CHANNELS as usize;
|
||||
assert_eq!(output_frames, expected_frames);
|
||||
assert_eq!(output.concat(), whole_output, "chunk_frames={chunk_frames}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_incomplete_interleaved_frames() {
|
||||
let mut resampler = AudioResampler::new(stereo_config()).unwrap();
|
||||
|
||||
assert!(resampler.process(&[TONE_AMPLITUDE]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpolates_mono_samples() {
|
||||
let config = AudioResamplerConfig {
|
||||
input_rate: UNIT_RATE,
|
||||
output_rate: DOUBLE_RATE,
|
||||
channels: MONO_CHANNELS,
|
||||
};
|
||||
let mut resampler = AudioResampler::new(config).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
resampler.process(&[0.0, 1.0, 2.0]).unwrap(),
|
||||
[0.0, 0.5, 1.0, 1.5, 2.0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_rate_configuration() {
|
||||
let config = AudioResamplerConfig {
|
||||
input_rate: 0,
|
||||
output_rate: OUTPUT_RATE,
|
||||
channels: CHANNELS,
|
||||
};
|
||||
|
||||
assert!(AudioResampler::new(config).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn downsamples_across_packet_boundaries() {
|
||||
let config = AudioResamplerConfig {
|
||||
input_rate: DOUBLE_RATE,
|
||||
output_rate: UNIT_RATE,
|
||||
channels: MONO_CHANNELS,
|
||||
};
|
||||
let mut resampler = AudioResampler::new(config).unwrap();
|
||||
let mut output = resampler.process(&FIRST_DOWNSAMPLE_PACKET).unwrap();
|
||||
output.extend(resampler.process(&SECOND_DOWNSAMPLE_PACKET).unwrap());
|
||||
|
||||
assert_eq!(output, EXPECTED_DOWNSAMPLED_OUTPUT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sender_resampler_emits_only_complete_continuous_frames() {
|
||||
let input = stereo_tone(CHUNK_FRAMES * CHUNK_COUNT);
|
||||
let mut resampler =
|
||||
FixedFrameAudioResampler::new(stereo_config(), OUTPUT_PACKET_FRAMES).unwrap();
|
||||
let output: Vec<_> = input
|
||||
.chunks(CHUNK_FRAMES * CHANNELS as usize)
|
||||
.flat_map(|chunk| resampler.process(chunk).unwrap())
|
||||
.collect();
|
||||
|
||||
assert!(output.len() >= MIN_CONTINUITY_PACKETS);
|
||||
assert!(output
|
||||
.iter()
|
||||
.all(|packet| packet.len() == OUTPUT_PACKET_FRAMES * CHANNELS as usize));
|
||||
assert!(maximum_boundary_residual(&output) <= MAX_BOUNDARY_RESIDUAL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sender_downsampling_preserves_packet_continuity() {
|
||||
let input_packet_frames = RATE_44_1_KHZ as usize / PACKETS_PER_SECOND;
|
||||
let input = stereo_tone_at_rate(input_packet_frames * CHUNK_COUNT, RATE_44_1_KHZ);
|
||||
let config = AudioResamplerConfig {
|
||||
input_rate: RATE_44_1_KHZ,
|
||||
output_rate: INPUT_RATE,
|
||||
channels: CHANNELS,
|
||||
};
|
||||
let mut resampler =
|
||||
FixedFrameAudioResampler::new(config, INPUT_RATE as usize / PACKETS_PER_SECOND).unwrap();
|
||||
let packets: Vec<_> = input
|
||||
.chunks(input_packet_frames * CHANNELS as usize)
|
||||
.flat_map(|packet| resampler.process(packet).unwrap())
|
||||
.collect();
|
||||
let residual = maximum_boundary_residual(&packets);
|
||||
|
||||
assert_eq!(packets.len(), CHUNK_COUNT);
|
||||
assert!(
|
||||
residual <= maximum_tone_prediction_residual(INPUT_RATE) + FLOAT_TOLERANCE,
|
||||
"sender packet boundary residual {residual} exceeded the tone curvature"
|
||||
);
|
||||
}
|
||||
396
src/client.rs
396
src/client.rs
@@ -30,7 +30,8 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
check_port,
|
||||
common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP},
|
||||
create_symmetric_key_msg, decode_id_pk, decode_id_pk_dtls, get_rs_pk, is_keyboard_mode_supported,
|
||||
create_symmetric_key_msg, decode_id_pk, decode_id_pk_dtls, dtls_fingerprint_bound, get_rs_pk,
|
||||
is_keyboard_mode_supported,
|
||||
kcp_stream::KcpStream,
|
||||
secure_tcp,
|
||||
ui_interface::{get_builtin_option, resolve_avatar_url, use_texture_render},
|
||||
@@ -93,6 +94,13 @@ use crate::ui_session_interface::SessionPermissionConfig;
|
||||
|
||||
pub use super::lang::*;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
mod audio_playback;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod audio_playback_recovery;
|
||||
#[cfg(all(test, not(target_os = "linux")))]
|
||||
#[path = "client/tests/audio_state_tests.rs"]
|
||||
mod audio_state_tests;
|
||||
pub mod file_trait;
|
||||
pub mod helper;
|
||||
pub mod io_loop;
|
||||
@@ -569,7 +577,7 @@ impl Client {
|
||||
return race_transports_prefer_webrtc(
|
||||
preferred_fut,
|
||||
vec![fallback_fut],
|
||||
Self::WEBRTC_PREFER_WINDOW_MS,
|
||||
Self::relay_fallback_delay_ms(),
|
||||
|result| result.0 .1,
|
||||
)
|
||||
.await;
|
||||
@@ -610,11 +618,27 @@ impl Client {
|
||||
/// ones that traverse NAT.
|
||||
const MAX_PENDING_WEBRTC_ICE: usize = 64;
|
||||
|
||||
/// Prefer-P2P window: how long a WebRTC attempt outranks an already-established relay
|
||||
/// result, and the floor for a punch-path WebRTC attempt whose race timeout is tuned for a
|
||||
/// raw TCP SYN. Long enough for candidate trickle + ICE checks + DTLS on high-latency
|
||||
/// links; short enough that UDP-blocked networks settle on relay without a noticeable wait.
|
||||
const WEBRTC_PREFER_WINDOW_MS: u64 = 2500;
|
||||
/// Default relay fallback delay: how long an already-established relay result is held back
|
||||
/// while a WebRTC attempt is still in flight, and the floor for a punch-path WebRTC attempt
|
||||
/// whose race timeout is tuned for a raw TCP SYN. Long enough for candidate trickle + ICE
|
||||
/// checks + DTLS on high-latency links; short enough that UDP-blocked networks settle on
|
||||
/// relay without a noticeable wait. The same role RFC 8305 calls a connection attempt delay.
|
||||
const RELAY_FALLBACK_DELAY_MS: u64 = 2500;
|
||||
|
||||
/// The delay as the user configured it, falling back to `RELAY_FALLBACK_DELAY_MS`. The
|
||||
/// settings field holds seconds, which is what a user reasons about; everything here is
|
||||
/// milliseconds. Unparseable, zero or negative all mean "unset", so clearing the field
|
||||
/// restores the default instead of collapsing the delay and handing every race to the
|
||||
/// relay.
|
||||
fn relay_fallback_delay_ms() -> u64 {
|
||||
match LocalConfig::get_option(keys::OPTION_RELAY_FALLBACK_DELAY)
|
||||
.trim()
|
||||
.parse::<f64>()
|
||||
{
|
||||
Ok(secs) if secs.is_finite() && secs > 0.0 => (secs * 1000.0).round() as u64,
|
||||
_ => Self::RELAY_FALLBACK_DELAY_MS,
|
||||
}
|
||||
}
|
||||
|
||||
/// UDP-NAT-test wait when the TCP clock is implausible (see TCP_RTT_PLAUSIBLE_MIN). The
|
||||
/// normal bound is `rtt / 2`: the test has been running since before the TCP connect, so on
|
||||
@@ -1114,7 +1138,7 @@ impl Client {
|
||||
race_transports_prefer_webrtc(
|
||||
webrtc_fut,
|
||||
connect_futures,
|
||||
Self::WEBRTC_PREFER_WINDOW_MS,
|
||||
Self::relay_fallback_delay_ms(),
|
||||
|result| result.3,
|
||||
)
|
||||
.await
|
||||
@@ -1442,7 +1466,7 @@ impl Client {
|
||||
// so a viable P2P path is not abandoned before it can complete; TCP/UDP keep the
|
||||
// tighter timeout, so a working direct connection still wins immediately, and the
|
||||
// relay fallback only waits the extra time when direct attempts all failed.
|
||||
let webrtc_timeout = connect_timeout.max(Self::WEBRTC_PREFER_WINDOW_MS);
|
||||
let webrtc_timeout = connect_timeout.max(Self::relay_fallback_delay_ms());
|
||||
async move {
|
||||
raced.wait_connected(webrtc_timeout).await?;
|
||||
// Resolve the pair here: a TURN win is relayed, not direct, and must be held
|
||||
@@ -1460,7 +1484,7 @@ impl Client {
|
||||
race_transports_prefer_webrtc(
|
||||
webrtc_fut,
|
||||
direct_futures,
|
||||
Self::WEBRTC_PREFER_WINDOW_MS,
|
||||
Self::relay_fallback_delay_ms(),
|
||||
|r| r.3,
|
||||
)
|
||||
.await
|
||||
@@ -1648,7 +1672,7 @@ impl Client {
|
||||
let actual_fp = conn.dtls_fingerprint(false).await.ok_or_else(
|
||||
|| anyhow!("WebRTC DTLS fingerprint unavailable"),
|
||||
)?;
|
||||
if signed_fp.is_empty() || signed_fp != actual_fp {
|
||||
if !dtls_fingerprint_bound(&signed_fp, &actual_fp) {
|
||||
bail!("WebRTC DTLS fingerprint not bound to peer identity (possible MITM)");
|
||||
}
|
||||
}
|
||||
@@ -2053,6 +2077,8 @@ pub struct AudioHandler {
|
||||
simple: Option<psimple::Simple>,
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
audio_buffer: AudioBuffer,
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
audio_resampler: Option<crate::audio_resampler::AudioResampler>,
|
||||
sample_rate: (u32, u32),
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
audio_stream: Option<Box<dyn StreamTrait>>,
|
||||
@@ -2060,7 +2086,57 @@ pub struct AudioHandler {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
device_channel: u16,
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
ready: Arc<std::sync::Mutex<bool>>,
|
||||
playback_status: Arc<audio_playback::AudioPlaybackStatus>,
|
||||
#[cfg(target_os = "windows")]
|
||||
playback_recovery: audio_playback_recovery::PlaybackRecovery,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[derive(Clone, Copy)]
|
||||
struct DecodedAudioConfig {
|
||||
sample_rate: u32,
|
||||
input_channels: u16,
|
||||
output_channels: u16,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn create_audio_resampler(
|
||||
input_rate: u32,
|
||||
output_rate: u32,
|
||||
channels: u16,
|
||||
) -> ResultType<Option<crate::audio_resampler::AudioResampler>> {
|
||||
if input_rate == output_rate {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(crate::audio_resampler::AudioResampler::new(
|
||||
crate::audio_resampler::AudioResamplerConfig {
|
||||
input_rate,
|
||||
output_rate,
|
||||
channels,
|
||||
},
|
||||
)?))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn prepare_decoded_audio(
|
||||
input: &[f32],
|
||||
resampler: Option<&mut crate::audio_resampler::AudioResampler>,
|
||||
config: DecodedAudioConfig,
|
||||
) -> Result<Vec<f32>, crate::audio_resampler::AudioResamplerError> {
|
||||
let mut output = match resampler {
|
||||
Some(resampler) => resampler.process(input)?,
|
||||
None => input.to_owned(),
|
||||
};
|
||||
if config.input_channels != config.output_channels {
|
||||
output = crate::audio_rechannel(
|
||||
output,
|
||||
config.sample_rate,
|
||||
config.sample_rate,
|
||||
config.input_channels,
|
||||
config.output_channels,
|
||||
);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
@@ -2068,6 +2144,7 @@ struct AudioBuffer(
|
||||
pub Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>,
|
||||
usize,
|
||||
[usize; 30],
|
||||
Arc<std::sync::atomic::AtomicUsize>,
|
||||
);
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
@@ -2079,6 +2156,7 @@ impl Default for AudioBuffer {
|
||||
)),
|
||||
48000 * 2,
|
||||
[0; 30],
|
||||
Arc::new(std::sync::atomic::AtomicUsize::new(0)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2153,27 +2231,38 @@ impl AudioBuffer {
|
||||
let skip = (cap * max / (30 * N) + 1) & (!1);
|
||||
if (having > skip * 3) && (skip > 0) {
|
||||
lock.skip(skip);
|
||||
log::info!("skip {skip}, based {max} {zero}");
|
||||
let generation = self.signal_discontinuity();
|
||||
drop(lock);
|
||||
log::info!("skip {skip}, based {max} {zero}, generation={generation}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The caller must hold the PCM buffer lock while signaling the discard.
|
||||
fn signal_discontinuity(&self) -> usize {
|
||||
self.3
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
.wrapping_add(1)
|
||||
}
|
||||
|
||||
/// append pcm to audio buffer, if buffered data
|
||||
/// exceeds AUDIO_BUFFER_MS, only AUDIO_BUFFER_MS
|
||||
/// will be kept.
|
||||
fn append_pcm2(&self, buffer: &[f32]) -> usize {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
let cap = lock.capacity();
|
||||
if buffer.len() > cap {
|
||||
lock.push_slice_overwrite(buffer);
|
||||
return cap;
|
||||
}
|
||||
|
||||
let having = lock.occupied_len() + buffer.len();
|
||||
if having > cap {
|
||||
lock.skip(having - cap);
|
||||
}
|
||||
lock.push_slice_overwrite(buffer);
|
||||
lock.occupied_len()
|
||||
let discard = (having > cap).then(|| (having - cap, self.signal_discontinuity()));
|
||||
let occupied = lock.occupied_len();
|
||||
drop(lock);
|
||||
if let Some((discarded, generation)) = discard {
|
||||
hbb_common::throttled_log!(
|
||||
audio_playback::AUDIO_PLAYBACK_LOG_INTERVAL,
|
||||
debug,
|
||||
"Audio buffer capacity discard: samples={discarded}, generation={generation}"
|
||||
);
|
||||
}
|
||||
occupied
|
||||
}
|
||||
|
||||
/// append pcm to audio buffer, trying to drop data
|
||||
@@ -2185,6 +2274,41 @@ impl AudioBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_os = "linux")))]
|
||||
mod audio_buffer_discontinuity_tests {
|
||||
use super::AudioBuffer;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
};
|
||||
|
||||
const BUFFER_CAPACITY: usize = 4;
|
||||
const BUFFER_LEVELS: usize = 30;
|
||||
const FIRST_INPUT: [f32; 2] = [0.1, 0.2];
|
||||
const OVERFLOWING_INPUT: [f32; 3] = [0.3, 0.4, 0.5];
|
||||
const OVERSIZED_INPUT: [f32; 5] = [0.6, 0.7, 0.8, 0.9, 1.0];
|
||||
|
||||
#[test]
|
||||
fn capacity_discards_signal_discontinuities() {
|
||||
let audio_buffer = AudioBuffer(
|
||||
Arc::new(Mutex::new(ringbuf::HeapRb::new(BUFFER_CAPACITY))),
|
||||
BUFFER_CAPACITY,
|
||||
[0; BUFFER_LEVELS],
|
||||
Arc::new(AtomicUsize::new(0)),
|
||||
);
|
||||
|
||||
assert_eq!(audio_buffer.append_pcm2(&FIRST_INPUT), FIRST_INPUT.len());
|
||||
assert_eq!(audio_buffer.3.load(Ordering::Relaxed), 0);
|
||||
assert_eq!(
|
||||
audio_buffer.append_pcm2(&OVERFLOWING_INPUT),
|
||||
BUFFER_CAPACITY
|
||||
);
|
||||
assert_eq!(audio_buffer.3.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(audio_buffer.append_pcm2(&OVERSIZED_INPUT), BUFFER_CAPACITY);
|
||||
assert_eq!(audio_buffer.3.load(Ordering::Relaxed), 2);
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioHandler {
|
||||
#[cfg(target_os = "linux")]
|
||||
fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> {
|
||||
@@ -2231,13 +2355,16 @@ impl AudioHandler {
|
||||
log::info!("Remote input format: {:?}", format0);
|
||||
#[allow(unused_mut)]
|
||||
let mut config: StreamConfig = config.into();
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
// this makes ios audio output not work
|
||||
// this makes ios and android audio output not work
|
||||
config.buffer_size = cpal::BufferSize::Fixed(64);
|
||||
}
|
||||
|
||||
self.sample_rate = (format0.sample_rate, config.sample_rate.0);
|
||||
let audio_resampler = create_audio_resampler(
|
||||
format0.sample_rate, config.sample_rate.0, format0.channels as _,
|
||||
)?;
|
||||
let mut build_output_stream = |config: StreamConfig| match sample_format {
|
||||
cpal::SampleFormat::I8 => self.build_output_stream::<i8>(&config, &device),
|
||||
cpal::SampleFormat::I16 => self.build_output_stream::<i16>(&config, &device),
|
||||
@@ -2262,22 +2389,61 @@ impl AudioHandler {
|
||||
} else {
|
||||
build_output_stream(config)?;
|
||||
}
|
||||
self.audio_resampler = audio_resampler;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle audio format and create an audio decoder.
|
||||
pub fn handle_format(&mut self, f: AudioFormat) {
|
||||
self.handle_format_with_start(f, Self::start_audio);
|
||||
}
|
||||
|
||||
fn handle_format_with_start(
|
||||
&mut self,
|
||||
f: AudioFormat,
|
||||
start: impl FnOnce(&mut Self, AudioFormat) -> ResultType<()>,
|
||||
) {
|
||||
if !is_supported_audio_channel_count(f.channels) {
|
||||
log::error!("Unsupported audio channel count: {}", f.channels);
|
||||
return;
|
||||
}
|
||||
match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) {
|
||||
Ok(d) => {
|
||||
#[cfg(target_os = "windows")]
|
||||
let playback_failed = self.cancel_pending_playback();
|
||||
#[cfg(target_os = "linux")]
|
||||
let keep_existing_stream = self.simple.is_some()
|
||||
&& self.sample_rate.0 == f.sample_rate
|
||||
&& u32::from(self.channels) == f.channels;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let keep_existing_stream = self.audio_stream.is_some()
|
||||
&& self.sample_rate.0 == f.sample_rate
|
||||
&& u32::from(self.channels) == f.channels;
|
||||
let buffer = vec![0.; f.sample_rate as usize * f.channels as usize];
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let mut previous = std::mem::take(self);
|
||||
#[cfg(target_os = "windows")]
|
||||
self.prepare_playback(&f);
|
||||
self.audio_decoder = Some((d, buffer));
|
||||
self.channels = f.channels as _;
|
||||
allow_err!(self.start_audio(f));
|
||||
let result = start(self, f);
|
||||
#[cfg(target_os = "windows")]
|
||||
let keep_existing_stream = keep_existing_stream
|
||||
&& !playback_failed
|
||||
&& !previous.playback_recovery.report_pending();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
if result.is_err() && keep_existing_stream {
|
||||
// The restarted capture has new Opus history even when output startup fails.
|
||||
previous.audio_decoder = self.audio_decoder.take();
|
||||
*self = previous;
|
||||
self.handle_audio_start_result(result, true);
|
||||
return;
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
self.finish_playback_replacement(result, keep_existing_stream.then_some(previous));
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
self.handle_audio_start_result(result, keep_existing_stream);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to create audio decoder: {}", err);
|
||||
@@ -2285,52 +2451,73 @@ impl AudioHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_audio_start_result(&mut self, result: ResultType<()>, keep_existing_stream: bool) {
|
||||
if let Err(error) = result {
|
||||
if keep_existing_stream {
|
||||
log::error!(
|
||||
"Failed to replace audio playback stream; keeping the existing compatible stream: {error:#}"
|
||||
);
|
||||
} else {
|
||||
*self = Self::default();
|
||||
log::error!("Failed to start audio playback: {error:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle audio frame and play it.
|
||||
#[inline]
|
||||
pub fn handle_frame(&mut self, frame: AudioFrame) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() {
|
||||
self.playback_status.report_errors();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
if self.audio_stream.is_none()
|
||||
|| !self
|
||||
.playback_status
|
||||
.ready
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
{
|
||||
return;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if self.simple.is_none() {
|
||||
log::debug!("PulseAudio simple binding does not exists");
|
||||
log::trace!("PulseAudio simple binding does not exists");
|
||||
return;
|
||||
}
|
||||
self.audio_decoder.as_mut().map(|(d, buffer)| {
|
||||
if let Ok(n) = d.decode_float(&frame.data, buffer, false) {
|
||||
let channels = self.channels;
|
||||
let n = n * (channels as usize);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let sample_rate0 = self.sample_rate.0;
|
||||
let sample_rate = self.sample_rate.1;
|
||||
let mut buffer = buffer[0..n].to_owned();
|
||||
if sample_rate != sample_rate0 {
|
||||
buffer = crate::audio_resample(
|
||||
&buffer[0..n],
|
||||
sample_rate0,
|
||||
sample_rate,
|
||||
channels,
|
||||
);
|
||||
}
|
||||
if self.channels != self.device_channel {
|
||||
buffer = crate::audio_rechannel(
|
||||
buffer,
|
||||
sample_rate,
|
||||
sample_rate,
|
||||
self.channels,
|
||||
self.device_channel,
|
||||
);
|
||||
}
|
||||
self.audio_buffer.append_pcm(&buffer);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let data_u8 =
|
||||
unsafe { std::slice::from_raw_parts::<u8>(buffer.as_ptr() as _, n * 4) };
|
||||
self.simple.as_mut().map(|x| x.write(data_u8));
|
||||
let decoded_frames = match d.decode_float(&frame.data, buffer, false) {
|
||||
Ok(decoded_frames) => decoded_frames,
|
||||
Err(error) => {
|
||||
log::warn!("Failed to decode audio frame: {error:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let channels = self.channels;
|
||||
let n = decoded_frames * channels as usize;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let config = DecodedAudioConfig {
|
||||
sample_rate: self.sample_rate.1,
|
||||
input_channels: self.channels,
|
||||
output_channels: self.device_channel,
|
||||
};
|
||||
let buffer = match prepare_decoded_audio(
|
||||
&buffer[0..n],
|
||||
self.audio_resampler.as_mut(),
|
||||
config,
|
||||
) {
|
||||
Ok(output) => output,
|
||||
Err(error) => {
|
||||
log::error!("Failed to resample decoded audio: {error:#}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.audio_buffer.append_pcm(&buffer);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let data_u8 =
|
||||
unsafe { std::slice::from_raw_parts::<u8>(buffer.as_ptr() as _, n * 4) };
|
||||
self.simple.as_mut().map(|x| x.write(data_u8));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2343,6 +2530,9 @@ impl AudioHandler {
|
||||
device: &Device,
|
||||
) -> ResultType<()> {
|
||||
self.device_channel = config.channels;
|
||||
#[cfg(target_os = "windows")]
|
||||
let err_fn = self.playback_recovery.new_error_callback();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let err_fn = move |err| {
|
||||
// too many errors, will improve later
|
||||
log::trace!("an error occurred on stream: {}", err);
|
||||
@@ -2350,63 +2540,28 @@ impl AudioHandler {
|
||||
self.audio_buffer
|
||||
.resize(config.sample_rate.0 as _, config.channels as _);
|
||||
let audio_buffer = self.audio_buffer.0.clone();
|
||||
let ready = self.ready.clone();
|
||||
let discontinuity_generation = self.audio_buffer.3.clone();
|
||||
let mut playback_writer = audio_playback::AudioPlaybackWriter::new(
|
||||
audio_playback::AudioPlaybackConfig {
|
||||
sample_rate: config.sample_rate.0,
|
||||
channels: config.channels as usize,
|
||||
},
|
||||
audio_buffer,
|
||||
discontinuity_generation,
|
||||
)?;
|
||||
let playback_status = playback_writer.status.clone();
|
||||
let timeout = None;
|
||||
let stream = device.build_output_stream(
|
||||
config,
|
||||
move |data: &mut [T], info: &cpal::OutputCallbackInfo| {
|
||||
if !*ready.lock().unwrap() {
|
||||
*ready.lock().unwrap() = true;
|
||||
}
|
||||
|
||||
let mut n = data.len();
|
||||
let mut lock = audio_buffer.lock().unwrap();
|
||||
let mut having = lock.occupied_len();
|
||||
// android two timestamps, one from zero, another not
|
||||
#[cfg(not(target_os = "android"))]
|
||||
if having < n {
|
||||
let tms = info.timestamp();
|
||||
let how_long = tms
|
||||
.playback
|
||||
.duration_since(&tms.callback)
|
||||
.unwrap_or(Duration::from_millis(0));
|
||||
|
||||
// must long enough to fight back scheuler delay
|
||||
if how_long > Duration::from_millis(6) && how_long < Duration::from_millis(3000)
|
||||
{
|
||||
drop(lock);
|
||||
std::thread::sleep(how_long.div_f32(1.2));
|
||||
lock = audio_buffer.lock().unwrap();
|
||||
having = lock.occupied_len();
|
||||
}
|
||||
|
||||
if having < n {
|
||||
n = having;
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
if having < n {
|
||||
n = having;
|
||||
}
|
||||
let mut elems = vec![0.0f32; n];
|
||||
if n > 0 {
|
||||
lock.pop_slice(&mut elems);
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
let mut input = elems.into_iter();
|
||||
for sample in data.iter_mut() {
|
||||
*sample = match input.next() {
|
||||
Some(x) => T::from_sample(x),
|
||||
_ => T::from_sample(0.),
|
||||
};
|
||||
}
|
||||
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
|
||||
playback_writer.write_output(data);
|
||||
},
|
||||
err_fn,
|
||||
timeout,
|
||||
)?;
|
||||
stream.play()?;
|
||||
self.audio_stream = Some(Box::new(stream));
|
||||
self.playback_status = playback_status;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -2426,6 +2581,27 @@ mod audio_format_tests {
|
||||
assert!(!is_supported_audio_channel_count(0));
|
||||
assert!(!is_supported_audio_channel_count(u32::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_audio_start_discards_format_state() {
|
||||
use super::{anyhow, AudioDecoder, AudioHandler, Stereo};
|
||||
|
||||
const SAMPLE_RATE: u32 = 48_000;
|
||||
const CHANNELS: u16 = 2;
|
||||
let decoder = AudioDecoder::new(SAMPLE_RATE, Stereo).unwrap();
|
||||
let mut handler = AudioHandler {
|
||||
audio_decoder: Some((decoder, Vec::new())),
|
||||
sample_rate: (SAMPLE_RATE, SAMPLE_RATE),
|
||||
channels: CHANNELS,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
handler.handle_audio_start_result(Err(anyhow!("Injected playback startup failure")), false);
|
||||
|
||||
assert!(handler.audio_decoder.is_none());
|
||||
assert_eq!(handler.channels, 0);
|
||||
assert_eq!(handler.sample_rate, (0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
/// Video handler for the [`Client`].
|
||||
@@ -3929,7 +4105,11 @@ pub fn start_audio_thread() -> MediaSender {
|
||||
std::thread::spawn(move || {
|
||||
let mut audio_handler = AudioHandler::default();
|
||||
loop {
|
||||
if let Ok(data) = audio_receiver.recv() {
|
||||
#[cfg(target_os = "windows")]
|
||||
let received = audio_handler.receive_audio(&audio_receiver);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let received = audio_receiver.recv();
|
||||
if let Ok(data) = received {
|
||||
match data {
|
||||
MediaData::AudioFrame(af) => {
|
||||
audio_handler.handle_frame(*af);
|
||||
|
||||
233
src/client/audio_playback.rs
Normal file
233
src/client/audio_playback.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
use hbb_common::{log, log_throttle::LogThrottle, thiserror};
|
||||
use ringbuf::{ring_buffer::RbBase, Rb};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
TryLockError,
|
||||
};
|
||||
|
||||
pub(super) const UNDERRUN_DECLICK_MS: usize = 5;
|
||||
pub(super) const AUDIO_PLAYBACK_LOG_INTERVAL: std::time::Duration =
|
||||
std::time::Duration::from_secs(5);
|
||||
const MILLISECONDS_PER_SECOND: usize = 1_000;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) struct AudioPlaybackConfig {
|
||||
pub sample_rate: u32,
|
||||
pub channels: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub(super) enum AudioPlaybackError {
|
||||
#[error(
|
||||
"invalid audio playback configuration: sample_rate={}, channels={}",
|
||||
.0.sample_rate, .0.channels
|
||||
)]
|
||||
InvalidConfig(AudioPlaybackConfig),
|
||||
#[error("audio playback frame has {samples} samples for {channels} channels")]
|
||||
IncompleteFrame { samples: usize, channels: usize },
|
||||
#[error("audio playback transition frame count overflow")]
|
||||
FrameCountOverflow,
|
||||
}
|
||||
|
||||
pub(super) struct AudioPlaybackRecovery {
|
||||
channels: usize,
|
||||
transition_frames: usize,
|
||||
transition_frame: usize,
|
||||
had_input: bool,
|
||||
transition_start: Vec<f32>,
|
||||
output_frame: Vec<f32>,
|
||||
}
|
||||
|
||||
pub(super) struct AudioPlaybackStatus {
|
||||
pub(super) ready: AtomicBool,
|
||||
contentions: AtomicUsize,
|
||||
contention_log_throttle: LogThrottle,
|
||||
buffer_poisoned: AtomicBool,
|
||||
}
|
||||
|
||||
impl Default for AudioPlaybackStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ready: AtomicBool::new(false),
|
||||
contentions: AtomicUsize::new(0),
|
||||
contention_log_throttle: LogThrottle::new(AUDIO_PLAYBACK_LOG_INTERVAL),
|
||||
buffer_poisoned: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioPlaybackStatus {
|
||||
pub(super) fn report_errors(&self) {
|
||||
if self.contentions.load(Ordering::Relaxed) != 0
|
||||
&& self.contention_log_throttle.due().is_some()
|
||||
{
|
||||
let contentions = self.contentions.swap(0, Ordering::Relaxed);
|
||||
log::debug!("Audio playback PCM buffer contention: callbacks={contentions}");
|
||||
}
|
||||
if self.buffer_poisoned.swap(false, Ordering::Relaxed) {
|
||||
log::error!("Audio playback stopped reading a poisoned PCM buffer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct AudioPlaybackWriter {
|
||||
audio_buffer: std::sync::Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>,
|
||||
discontinuity_generation: std::sync::Arc<AtomicUsize>,
|
||||
observed_discontinuity_generation: usize,
|
||||
buffered_input: Vec<f32>,
|
||||
recovery: AudioPlaybackRecovery,
|
||||
pub(super) status: std::sync::Arc<AudioPlaybackStatus>,
|
||||
buffer_failed: bool,
|
||||
}
|
||||
|
||||
impl AudioPlaybackWriter {
|
||||
pub(super) fn new(
|
||||
config: AudioPlaybackConfig,
|
||||
audio_buffer: std::sync::Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>,
|
||||
discontinuity_generation: std::sync::Arc<AtomicUsize>,
|
||||
) -> Result<Self, AudioPlaybackError> {
|
||||
let recovery = AudioPlaybackRecovery::new(config)?;
|
||||
let buffer_capacity = audio_buffer.lock().unwrap().capacity();
|
||||
let observed_discontinuity_generation = discontinuity_generation.load(Ordering::Relaxed);
|
||||
Ok(Self {
|
||||
audio_buffer,
|
||||
discontinuity_generation,
|
||||
observed_discontinuity_generation,
|
||||
buffered_input: vec![0.0; buffer_capacity],
|
||||
recovery,
|
||||
status: Default::default(),
|
||||
buffer_failed: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_buffer(&mut self, requested_samples: usize) -> usize {
|
||||
if self.buffer_failed {
|
||||
return 0;
|
||||
}
|
||||
let mut buffer = match self.audio_buffer.try_lock() {
|
||||
Ok(buffer) => buffer,
|
||||
Err(TryLockError::WouldBlock) => {
|
||||
// Keep queued PCM and its generation for the next successful read.
|
||||
self.status.contentions.fetch_add(1, Ordering::Relaxed);
|
||||
return 0;
|
||||
}
|
||||
Err(TryLockError::Poisoned(_)) => {
|
||||
self.buffer_failed = true;
|
||||
self.status.ready.store(false, Ordering::Release);
|
||||
self.status.buffer_poisoned.store(true, Ordering::Relaxed);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let generation = self.discontinuity_generation.load(Ordering::Relaxed);
|
||||
let channels = self.recovery.channels;
|
||||
let samples = buffer.occupied_len().min(requested_samples) / channels * channels;
|
||||
buffer.pop_slice(&mut self.buffered_input[..samples]);
|
||||
drop(buffer);
|
||||
if generation != self.observed_discontinuity_generation {
|
||||
self.recovery.begin_discontinuity();
|
||||
self.observed_discontinuity_generation = generation;
|
||||
}
|
||||
samples
|
||||
}
|
||||
|
||||
pub(super) fn write_output<T>(&mut self, output: &mut [T])
|
||||
where
|
||||
T: cpal::Sample + cpal::FromSample<f32>,
|
||||
{
|
||||
self.status
|
||||
.ready
|
||||
.store(!self.buffer_failed, Ordering::Release);
|
||||
let requested_samples = output.len().min(self.buffered_input.len());
|
||||
let channel_count = self.recovery.channels;
|
||||
let available_samples = self.read_buffer(requested_samples);
|
||||
let available_frames = available_samples / channel_count;
|
||||
for (frame_index, output_frame) in output.chunks_mut(channel_count).enumerate() {
|
||||
let input = if frame_index < available_frames {
|
||||
let start = frame_index * channel_count;
|
||||
Some(&self.buffered_input[start..start + channel_count])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match self.recovery.process_frame(input) {
|
||||
Ok(recovered) => {
|
||||
for (output, sample) in output_frame.iter_mut().zip(recovered) {
|
||||
*output = T::from_sample(*sample);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("Failed to recover audio underflow: {error}");
|
||||
output_frame.fill(T::from_sample(0.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioPlaybackRecovery {
|
||||
pub(super) fn new(config: AudioPlaybackConfig) -> Result<Self, AudioPlaybackError> {
|
||||
if config.sample_rate == 0 || config.channels == 0 {
|
||||
return Err(AudioPlaybackError::InvalidConfig(config));
|
||||
}
|
||||
let transition_frames = (config.sample_rate as usize)
|
||||
.checked_mul(UNDERRUN_DECLICK_MS)
|
||||
.ok_or(AudioPlaybackError::FrameCountOverflow)?
|
||||
/ MILLISECONDS_PER_SECOND;
|
||||
if transition_frames == 0 {
|
||||
return Err(AudioPlaybackError::InvalidConfig(config));
|
||||
}
|
||||
Ok(Self {
|
||||
channels: config.channels,
|
||||
transition_frames,
|
||||
transition_frame: transition_frames,
|
||||
had_input: false,
|
||||
transition_start: vec![0.0; config.channels],
|
||||
output_frame: vec![0.0; config.channels],
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn process_frame(
|
||||
&mut self,
|
||||
input: Option<&[f32]>,
|
||||
) -> Result<&[f32], AudioPlaybackError> {
|
||||
if input.is_some_and(|frame| frame.len() != self.channels) {
|
||||
return Err(AudioPlaybackError::IncompleteFrame {
|
||||
samples: input.map_or(0, <[f32]>::len),
|
||||
channels: self.channels,
|
||||
});
|
||||
}
|
||||
self.begin_transition(input.is_some());
|
||||
let target_weight = self.advance_transition();
|
||||
for channel in 0..self.channels {
|
||||
let target = input.map_or(0.0, |frame| frame[channel]);
|
||||
self.output_frame[channel] =
|
||||
self.transition_start[channel] * (1.0 - target_weight) + target * target_weight;
|
||||
}
|
||||
Ok(&self.output_frame)
|
||||
}
|
||||
|
||||
pub(super) fn begin_discontinuity(&mut self) {
|
||||
self.transition_start.copy_from_slice(&self.output_frame);
|
||||
self.transition_frame = 0;
|
||||
}
|
||||
|
||||
fn begin_transition(&mut self, has_input: bool) {
|
||||
if has_input == self.had_input {
|
||||
return;
|
||||
}
|
||||
self.transition_start.copy_from_slice(&self.output_frame);
|
||||
self.transition_frame = 0;
|
||||
self.had_input = has_input;
|
||||
}
|
||||
|
||||
fn advance_transition(&mut self) -> f32 {
|
||||
if self.transition_frame >= self.transition_frames {
|
||||
return 1.0;
|
||||
}
|
||||
self.transition_frame += 1;
|
||||
self.transition_frame as f32 / self.transition_frames as f32
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "audio_playback_tests.rs"]
|
||||
mod tests;
|
||||
184
src/client/audio_playback_recovery.rs
Normal file
184
src/client/audio_playback_recovery.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use super::{AudioDecoder, AudioFormat, AudioHandler, MediaData, Mono, Stereo};
|
||||
use cpal::StreamError;
|
||||
use crossbeam_queue::SegQueue;
|
||||
use hbb_common::{log, tokio::time::Instant, ResultType};
|
||||
use std::{
|
||||
sync::{atomic::Ordering, mpsc, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const RECOVERY_INTERVAL: Duration = Duration::from_secs(1);
|
||||
pub(super) const STARTUP_CONFIRMATION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
// The pinned WASAPI backend reports this warning but keeps its worker running.
|
||||
const PRIORITY_WARNING_PREFIX: &str = "SetThreadPriority failed: ";
|
||||
|
||||
#[path = "audio_playback_startup.rs"]
|
||||
mod startup;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct PlaybackRecovery {
|
||||
pub(super) errors: Arc<SegQueue<StreamError>>,
|
||||
format: Option<AudioFormat>,
|
||||
pub(super) retry_at: Option<Instant>,
|
||||
restart_not_before: Option<Instant>,
|
||||
awaiting_callback: bool,
|
||||
startup_deadline: Option<Instant>,
|
||||
pending_output: Option<Box<AudioHandler>>,
|
||||
}
|
||||
|
||||
impl PlaybackRecovery {
|
||||
pub(super) fn new_error_callback(&mut self) -> impl FnMut(StreamError) + Send + 'static {
|
||||
self.errors = Default::default();
|
||||
let errors = self.errors.clone();
|
||||
move |error| errors.push(error)
|
||||
}
|
||||
|
||||
pub(super) fn report_pending(&self) -> bool {
|
||||
let mut failed = false;
|
||||
while let Some(error) = self.errors.pop() {
|
||||
if matches!(&error, StreamError::BackendSpecific { err }
|
||||
if err.description.starts_with(PRIORITY_WARNING_PREFIX))
|
||||
{
|
||||
log::warn!("Audio playback nonterminal priority warning: {error}");
|
||||
} else {
|
||||
log::error!("Audio playback stream failed: {error}");
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
failed
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioHandler {
|
||||
fn clear_playback_stream(&mut self) {
|
||||
// Dropping CPAL may join its worker; run this on the owner, not its callback.
|
||||
self.audio_stream = None;
|
||||
self.playback_recovery.report_pending();
|
||||
self.playback_status.report_errors();
|
||||
let recovery = std::mem::take(self).playback_recovery;
|
||||
self.playback_recovery.format = recovery.format;
|
||||
self.playback_recovery.retry_at = recovery.retry_at;
|
||||
self.playback_recovery.restart_not_before = recovery.restart_not_before;
|
||||
}
|
||||
|
||||
pub(super) fn prepare_playback(&mut self, format: &AudioFormat) {
|
||||
self.clear_playback_stream();
|
||||
self.playback_recovery.format = Some(format.clone());
|
||||
self.playback_recovery.retry_at = None;
|
||||
self.playback_recovery.restart_not_before = None;
|
||||
}
|
||||
|
||||
pub(super) fn finish_playback_start(&mut self, result: ResultType<()>) {
|
||||
let now = Instant::now();
|
||||
let retry_at = now + RECOVERY_INTERVAL;
|
||||
self.playback_recovery.restart_not_before = Some(retry_at);
|
||||
match result {
|
||||
Ok(()) => {
|
||||
self.playback_recovery.retry_at = None;
|
||||
self.playback_recovery.awaiting_callback = true;
|
||||
self.playback_recovery.startup_deadline = Some(now + STARTUP_CONFIRMATION_TIMEOUT);
|
||||
log::info!("Audio playback stream opened; waiting for output callback");
|
||||
}
|
||||
Err(error) => {
|
||||
self.clear_playback_stream();
|
||||
self.playback_recovery.retry_at = Some(retry_at);
|
||||
log::error!(
|
||||
"Audio playback start failed: {error:#}; retrying in {RECOVERY_INTERVAL:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn playback_start_timed_out(&mut self, now: Instant) -> bool {
|
||||
if !self.playback_recovery.awaiting_callback
|
||||
|| self.playback_status.ready.load(Ordering::Acquire)
|
||||
|| !self
|
||||
.playback_recovery
|
||||
.startup_deadline
|
||||
.is_some_and(|due| now >= due)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.playback_recovery.awaiting_callback = false;
|
||||
self.playback_recovery.startup_deadline = None;
|
||||
log::error!("Audio playback start timed out waiting for output callback");
|
||||
true
|
||||
}
|
||||
|
||||
fn restart_playback(&mut self, format: AudioFormat) -> ResultType<()> {
|
||||
let channels = if format.channels > 1 { Stereo } else { Mono };
|
||||
let decoder = AudioDecoder::new(format.sample_rate, channels)?;
|
||||
let buffer = vec![0.; format.sample_rate as usize * format.channels as usize];
|
||||
let channel_count = format.channels as _;
|
||||
self.start_audio(format)?;
|
||||
self.channels = channel_count;
|
||||
self.audio_decoder = Some((decoder, buffer));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn recover_playback_with(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
restart: impl FnOnce(&mut Self, AudioFormat) -> ResultType<()>,
|
||||
) {
|
||||
let failed = self.resolve_pending_playback(now).unwrap_or_else(|| {
|
||||
self.playback_recovery.report_pending() || self.playback_start_timed_out(now)
|
||||
});
|
||||
if failed {
|
||||
self.clear_playback_stream();
|
||||
self.playback_recovery.retry_at = Some(
|
||||
self.playback_recovery
|
||||
.restart_not_before
|
||||
.map_or(now, |due| due.max(now)),
|
||||
);
|
||||
}
|
||||
if self.playback_recovery.awaiting_callback
|
||||
&& self.playback_status.ready.load(Ordering::Acquire)
|
||||
{
|
||||
self.playback_recovery.awaiting_callback = false;
|
||||
self.playback_recovery.startup_deadline = None;
|
||||
log::info!("Audio playback output callback started");
|
||||
}
|
||||
if !self
|
||||
.playback_recovery
|
||||
.retry_at
|
||||
.is_some_and(|due| now >= due)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(format) = self.playback_recovery.format.clone() else {
|
||||
return;
|
||||
};
|
||||
log::info!("Recreating audio playback on the current default output device");
|
||||
let result = restart(self, format);
|
||||
self.finish_playback_start(result);
|
||||
}
|
||||
|
||||
pub(super) fn receive_audio(
|
||||
&mut self,
|
||||
receiver: &mpsc::Receiver<MediaData>,
|
||||
) -> Result<MediaData, mpsc::RecvError> {
|
||||
receive_with_recovery(receiver, RECOVERY_INTERVAL, || {
|
||||
self.recover_playback_with(Instant::now(), Self::restart_playback);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn receive_with_recovery(
|
||||
receiver: &mpsc::Receiver<MediaData>,
|
||||
interval: Duration,
|
||||
mut recover: impl FnMut(),
|
||||
) -> Result<MediaData, mpsc::RecvError> {
|
||||
loop {
|
||||
match receiver.recv_timeout(interval) {
|
||||
Ok(data) => {
|
||||
if !matches!(data, MediaData::AudioFormat(_)) {
|
||||
recover();
|
||||
}
|
||||
return Ok(data);
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => recover(),
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => return Err(mpsc::RecvError),
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/client/audio_playback_startup.rs
Normal file
63
src/client/audio_playback_startup.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use super::{AudioHandler, Instant, Ordering, ResultType};
|
||||
use hbb_common::log;
|
||||
|
||||
impl AudioHandler {
|
||||
pub(in crate::client) fn cancel_pending_playback(&mut self) -> bool {
|
||||
// Format messages bypass recovery; retain a usable candidate before superseding it.
|
||||
let failed = self
|
||||
.resolve_pending_playback(Instant::now())
|
||||
.unwrap_or(false);
|
||||
if let Some(mut pending) = self.playback_recovery.pending_output.take() {
|
||||
pending.audio_stream = None;
|
||||
pending.playback_recovery.report_pending();
|
||||
pending.playback_status.report_errors();
|
||||
}
|
||||
failed
|
||||
}
|
||||
|
||||
pub(in crate::client) fn finish_playback_replacement(
|
||||
&mut self,
|
||||
result: ResultType<()>,
|
||||
previous: Option<Self>,
|
||||
) {
|
||||
self.finish_playback_start(result);
|
||||
let Some(mut previous) = previous else {
|
||||
return;
|
||||
};
|
||||
previous.audio_decoder = self.audio_decoder.take();
|
||||
let candidate = std::mem::replace(self, previous);
|
||||
self.playback_recovery.pending_output = Some(Box::new(candidate));
|
||||
log::info!("Audio playback replacement pending; continuing on the compatible output");
|
||||
self.recover_playback_with(Instant::now(), Self::restart_playback);
|
||||
}
|
||||
|
||||
pub(super) fn resolve_pending_playback(&mut self, now: Instant) -> Option<bool> {
|
||||
let mut candidate = self.playback_recovery.pending_output.take()?;
|
||||
let candidate_failed =
|
||||
candidate.playback_recovery.report_pending() || candidate.playback_start_timed_out(now);
|
||||
let previous_failed =
|
||||
self.playback_recovery.report_pending() || self.playback_start_timed_out(now);
|
||||
if candidate_failed {
|
||||
self.playback_recovery.restart_not_before =
|
||||
candidate.playback_recovery.restart_not_before;
|
||||
candidate.audio_stream = None;
|
||||
candidate.playback_recovery.report_pending();
|
||||
candidate.playback_status.report_errors();
|
||||
if !previous_failed {
|
||||
log::error!("Audio playback replacement failed before startup confirmation; keeping the existing compatible stream");
|
||||
}
|
||||
return Some(previous_failed);
|
||||
}
|
||||
if candidate.playback_status.ready.load(Ordering::Acquire) || previous_failed {
|
||||
candidate.audio_decoder = self.audio_decoder.take();
|
||||
self.audio_stream = None;
|
||||
self.playback_recovery.report_pending();
|
||||
self.playback_status.report_errors();
|
||||
*self = *candidate;
|
||||
return Some(false);
|
||||
}
|
||||
self.playback_recovery.pending_output = Some(candidate);
|
||||
// A second active-queue read could discard a healthy pending candidate.
|
||||
Some(false)
|
||||
}
|
||||
}
|
||||
262
src/client/audio_playback_tests.rs
Normal file
262
src/client/audio_playback_tests.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use super::{
|
||||
AudioPlaybackConfig, AudioPlaybackError, AudioPlaybackRecovery, AudioPlaybackStatus,
|
||||
AudioPlaybackWriter,
|
||||
};
|
||||
use ringbuf::{ring_buffer::RbBase, Rb};
|
||||
use std::{
|
||||
sync::{atomic::Ordering, mpsc, Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const SAMPLE_RATE: u32 = 48_000;
|
||||
const CHANNELS: usize = 2;
|
||||
const ACTIVE_FRAME: [f32; CHANNELS] = [0.8, -0.8];
|
||||
const OPPOSITE_ACTIVE_FRAME: [f32; CHANNELS] = [-0.8, 0.8];
|
||||
const ACTIVE_FRAMES: usize = 300;
|
||||
const SILENT_FRAMES: usize = 300;
|
||||
const TRANSITION_FRAMES: usize =
|
||||
SAMPLE_RATE as usize * super::UNDERRUN_DECLICK_MS / super::MILLISECONDS_PER_SECOND;
|
||||
const MAX_SAMPLE_STEP: f32 = 0.01;
|
||||
|
||||
#[test]
|
||||
fn writing_audio_observes_discard_and_releases_buffer_lock() {
|
||||
const INPUT: [f32; 4] = [0.1, 0.2, 0.3, 0.4];
|
||||
const GENERATION: usize = 7;
|
||||
let buffer = Arc::new(Mutex::new(ringbuf::HeapRb::new(INPUT.len())));
|
||||
let generation = Arc::new(super::AtomicUsize::new(0));
|
||||
let config = AudioPlaybackConfig {
|
||||
sample_rate: SAMPLE_RATE,
|
||||
channels: CHANNELS,
|
||||
};
|
||||
let mut writer = AudioPlaybackWriter::new(config, buffer.clone(), generation.clone()).unwrap();
|
||||
{
|
||||
let mut buffer = buffer.lock().unwrap();
|
||||
buffer.push_slice(&INPUT);
|
||||
generation.store(GENERATION, Ordering::Relaxed);
|
||||
}
|
||||
let mut output = [0.0_f32; INPUT.len()];
|
||||
|
||||
writer.write_output(&mut output);
|
||||
|
||||
assert_eq!(writer.buffered_input, INPUT);
|
||||
assert_eq!(writer.observed_discontinuity_generation, GENERATION);
|
||||
assert_eq!(buffer.try_lock().unwrap().occupied_len(), 0);
|
||||
}
|
||||
|
||||
fn maximum_sample_step(samples: &[f32]) -> f32 {
|
||||
samples
|
||||
.windows(CHANNELS + 1)
|
||||
.map(|window| (window[CHANNELS] - window[0]).abs())
|
||||
.fold(0.0, f32::max)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smooths_underflow_and_explicit_audio_discontinuities() {
|
||||
for explicit_discontinuity in [false, true] {
|
||||
let config = AudioPlaybackConfig {
|
||||
sample_rate: SAMPLE_RATE,
|
||||
channels: CHANNELS,
|
||||
};
|
||||
let mut recovery = AudioPlaybackRecovery::new(config).unwrap();
|
||||
let mut output = Vec::new();
|
||||
for _ in 0..ACTIVE_FRAMES {
|
||||
output.extend_from_slice(recovery.process_frame(Some(&ACTIVE_FRAME)).unwrap());
|
||||
}
|
||||
let transition_end = TRANSITION_FRAMES * CHANNELS;
|
||||
assert_eq!(
|
||||
&output[transition_end - CHANNELS..transition_end],
|
||||
ACTIVE_FRAME.as_slice(),
|
||||
"explicit_discontinuity={explicit_discontinuity}"
|
||||
);
|
||||
let resumed_frame = if explicit_discontinuity {
|
||||
recovery.begin_discontinuity();
|
||||
&OPPOSITE_ACTIVE_FRAME
|
||||
} else {
|
||||
for _ in 0..SILENT_FRAMES {
|
||||
output.extend_from_slice(recovery.process_frame(None).unwrap());
|
||||
}
|
||||
&ACTIVE_FRAME
|
||||
};
|
||||
for _ in 0..ACTIVE_FRAMES {
|
||||
output.extend_from_slice(recovery.process_frame(Some(resumed_frame)).unwrap());
|
||||
}
|
||||
let maximum = maximum_sample_step(&output);
|
||||
assert!(
|
||||
maximum <= MAX_SAMPLE_STEP,
|
||||
"step {maximum} exceeded {MAX_SAMPLE_STEP}, explicit={explicit_discontinuity}"
|
||||
);
|
||||
assert_eq!(
|
||||
&output[output.len() - CHANNELS..],
|
||||
resumed_frame,
|
||||
"explicit_discontinuity={explicit_discontinuity}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_configuration_and_frame_size() {
|
||||
let invalid_config = AudioPlaybackConfig {
|
||||
sample_rate: 0,
|
||||
channels: CHANNELS,
|
||||
};
|
||||
assert_eq!(
|
||||
AudioPlaybackRecovery::new(invalid_config).err(),
|
||||
Some(AudioPlaybackError::InvalidConfig(invalid_config))
|
||||
);
|
||||
|
||||
let config = AudioPlaybackConfig {
|
||||
sample_rate: SAMPLE_RATE,
|
||||
channels: CHANNELS,
|
||||
};
|
||||
let mut recovery = AudioPlaybackRecovery::new(config).unwrap();
|
||||
assert_eq!(
|
||||
recovery.process_frame(Some(&[0.5])).err(),
|
||||
Some(AudioPlaybackError::IncompleteFrame {
|
||||
samples: 1,
|
||||
channels: CHANNELS,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const CALLBACK_SAMPLES: usize = 64;
|
||||
const CALLBACK_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
const DISCARD_GENERATION: usize = 1;
|
||||
|
||||
fn write_while_buffer_is_locked(
|
||||
mut writer: AudioPlaybackWriter,
|
||||
buffer: &Arc<Mutex<ringbuf::HeapRb<f32>>>,
|
||||
generation: &Arc<super::AtomicUsize>,
|
||||
) -> (AudioPlaybackWriter, [f32; CALLBACK_SAMPLES]) {
|
||||
let mut guard = buffer.lock().unwrap();
|
||||
let queued = OPPOSITE_ACTIVE_FRAME.repeat(ACTIVE_FRAMES);
|
||||
guard.push_slice(&queued);
|
||||
generation.store(DISCARD_GENERATION, Ordering::Relaxed);
|
||||
let (completed_tx, completed_rx) = mpsc::channel();
|
||||
let callback = std::thread::spawn(move || {
|
||||
let mut output = [0.0; CALLBACK_SAMPLES];
|
||||
crate::audio_resampler::allocation_tests::assert_no_allocations(|| {
|
||||
writer.write_output(&mut output);
|
||||
});
|
||||
completed_tx.send((writer, output)).unwrap();
|
||||
});
|
||||
let completed = completed_rx.recv_timeout(CALLBACK_TIMEOUT);
|
||||
let retained = guard.occupied_len();
|
||||
drop(guard);
|
||||
callback.join().unwrap();
|
||||
let result = completed.expect("playback callback waited for the buffer owner");
|
||||
assert_eq!(retained, queued.len());
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playback_contention_preserves_queued_audio_and_recovers_after_release() {
|
||||
let samples = ACTIVE_FRAMES * CHANNELS;
|
||||
let buffer = Arc::new(Mutex::new(ringbuf::HeapRb::new(samples)));
|
||||
let generation = Arc::new(super::AtomicUsize::new(0));
|
||||
let config = AudioPlaybackConfig {
|
||||
sample_rate: SAMPLE_RATE,
|
||||
channels: CHANNELS,
|
||||
};
|
||||
let mut writer = AudioPlaybackWriter::new(config, buffer.clone(), generation.clone()).unwrap();
|
||||
buffer
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push_slice(&ACTIVE_FRAME.repeat(ACTIVE_FRAMES));
|
||||
let mut output = vec![0.0; samples];
|
||||
writer.write_output(&mut output);
|
||||
assert_eq!(&output[samples - CHANNELS..], &ACTIVE_FRAME);
|
||||
|
||||
let (mut writer, gap) = write_while_buffer_is_locked(writer, &buffer, &generation);
|
||||
|
||||
assert_eq!(writer.status.contentions.load(Ordering::Relaxed), 1);
|
||||
assert!(writer.status.ready.load(Ordering::Acquire));
|
||||
assert_eq!(writer.observed_discontinuity_generation, 0);
|
||||
assert!(maximum_sample_step(&gap) <= MAX_SAMPLE_STEP);
|
||||
assert!(gap[0] > 0.0 && gap[0] < ACTIVE_FRAME[0]);
|
||||
assert_eq!(gap[1], -gap[0]);
|
||||
assert!(gap[CALLBACK_SAMPLES - CHANNELS] > 0.0);
|
||||
writer.write_output(&mut output);
|
||||
let mut transition = gap[gap.len() - CHANNELS..].to_vec();
|
||||
transition.extend_from_slice(&output[..TRANSITION_FRAMES * CHANNELS]);
|
||||
assert!(maximum_sample_step(&transition) <= MAX_SAMPLE_STEP);
|
||||
assert_eq!(
|
||||
writer.buffered_input,
|
||||
OPPOSITE_ACTIVE_FRAME.repeat(ACTIVE_FRAMES)
|
||||
);
|
||||
assert_eq!(writer.observed_discontinuity_generation, DISCARD_GENERATION);
|
||||
assert_eq!(&output[samples - CHANNELS..], &OPPOSITE_ACTIVE_FRAME);
|
||||
assert_eq!(buffer.lock().unwrap().occupied_len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poisoned_playback_buffer_reports_once_without_panicking_in_the_callback() {
|
||||
let buffer = Arc::new(Mutex::new(ringbuf::HeapRb::new(CALLBACK_SAMPLES)));
|
||||
let config = AudioPlaybackConfig {
|
||||
sample_rate: SAMPLE_RATE,
|
||||
channels: CHANNELS,
|
||||
};
|
||||
let mut writer =
|
||||
AudioPlaybackWriter::new(config, buffer.clone(), Arc::new(super::AtomicUsize::new(0)))
|
||||
.unwrap();
|
||||
assert!(std::thread::spawn(move || {
|
||||
let _guard = buffer.lock().unwrap();
|
||||
panic!("Injected PCM buffer failure");
|
||||
})
|
||||
.join()
|
||||
.is_err());
|
||||
let mut output = [ACTIVE_FRAME[0]; CALLBACK_SAMPLES];
|
||||
|
||||
crate::audio_resampler::allocation_tests::assert_no_allocations(|| {
|
||||
writer.write_output(&mut output);
|
||||
});
|
||||
|
||||
assert_eq!(output, [0.0; CALLBACK_SAMPLES]);
|
||||
assert!(!writer.status.ready.load(Ordering::Acquire));
|
||||
assert!(writer.status.buffer_poisoned.load(Ordering::Relaxed));
|
||||
writer.status.report_errors();
|
||||
writer.write_output(&mut output);
|
||||
assert!(!writer.status.buffer_poisoned.load(Ordering::Relaxed));
|
||||
assert_eq!(writer.status.contentions.load(Ordering::Relaxed), 0);
|
||||
assert!(!writer.status.ready.load(Ordering::Acquire));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contention_counts_accumulate_until_the_next_report() {
|
||||
let status = AudioPlaybackStatus::default();
|
||||
status.report_errors();
|
||||
status.contentions.fetch_add(1, Ordering::Relaxed);
|
||||
status.report_errors();
|
||||
assert_eq!(status.contentions.load(Ordering::Relaxed), 0);
|
||||
|
||||
let mut total = 0;
|
||||
for callbacks in [3, 7, 2] {
|
||||
total += callbacks;
|
||||
status.contentions.fetch_add(callbacks, Ordering::Relaxed);
|
||||
status.report_errors();
|
||||
assert_eq!(status.contentions.load(Ordering::Relaxed), total);
|
||||
}
|
||||
status.buffer_poisoned.store(true, Ordering::Relaxed);
|
||||
status.report_errors();
|
||||
assert!(!status.buffer_poisoned.load(Ordering::Relaxed));
|
||||
assert_eq!(status.contentions.load(Ordering::Relaxed), total);
|
||||
|
||||
std::thread::sleep(super::AUDIO_PLAYBACK_LOG_INTERVAL);
|
||||
status.report_errors();
|
||||
assert_eq!(status.contentions.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contention_reporting_is_independent_between_playbacks() {
|
||||
let first = AudioPlaybackStatus::default();
|
||||
let second = AudioPlaybackStatus::default();
|
||||
for status in [&first, &second] {
|
||||
status.contentions.fetch_add(1, Ordering::Relaxed);
|
||||
status.report_errors();
|
||||
assert_eq!(status.contentions.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
for status in [&first, &second] {
|
||||
status.contentions.fetch_add(1, Ordering::Relaxed);
|
||||
status.report_errors();
|
||||
assert_eq!(status.contentions.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
}
|
||||
@@ -598,6 +598,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
} else {
|
||||
log::debug!("Failed to record local audio channel: {}", err);
|
||||
}
|
||||
// Both arms fall through with nothing else in this loop blocking, so
|
||||
// without a pause the thread spun a core for the whole voice call.
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
226
src/client/tests/audio_playback_recovery_tests.rs
Normal file
226
src/client/tests/audio_playback_recovery_tests.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use super::*;
|
||||
use crate::client::{
|
||||
audio_playback::AudioPlaybackStatus, audio_playback_recovery::STARTUP_CONFIRMATION_TIMEOUT,
|
||||
};
|
||||
use cpal::StreamError;
|
||||
use crossbeam_queue::SegQueue;
|
||||
use hbb_common::tokio::time::Instant;
|
||||
use std::time::Duration;
|
||||
|
||||
const AFTER_COOLDOWN: Duration = Duration::from_secs(2);
|
||||
|
||||
type PendingOutput = (
|
||||
Arc<AtomicBool>,
|
||||
Arc<AudioPlaybackStatus>,
|
||||
Arc<SegQueue<StreamError>>,
|
||||
);
|
||||
|
||||
fn install_output(handler: &mut AudioHandler, dropped: Arc<AtomicBool>) {
|
||||
handler.sample_rate = (INPUT_RATE, OUTPUT_RATE);
|
||||
handler.device_channel = CHANNELS;
|
||||
handler.audio_stream = Some(Box::new(TrackedAudioStream(dropped)));
|
||||
handler.playback_status.ready.store(true, Ordering::Release);
|
||||
}
|
||||
|
||||
fn recovery_handler() -> (AudioHandler, Arc<AtomicBool>) {
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
let mut handler = AudioHandler::default();
|
||||
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
|
||||
install_output(candidate, dropped.clone());
|
||||
Ok(())
|
||||
});
|
||||
(handler, dropped)
|
||||
}
|
||||
|
||||
fn begin_pending(handler: &mut AudioHandler) -> PendingOutput {
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
let mut state = None;
|
||||
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
|
||||
install_output(candidate, dropped.clone());
|
||||
candidate
|
||||
.playback_status
|
||||
.ready
|
||||
.store(false, Ordering::Release);
|
||||
state = Some((
|
||||
candidate.playback_status.clone(),
|
||||
candidate.playback_recovery.errors.clone(),
|
||||
));
|
||||
Ok(())
|
||||
});
|
||||
let (status, errors) = state.unwrap();
|
||||
(dropped, status, errors)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unconfirmed_start_retries_without_callback_or_error() {
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
let mut handler = AudioHandler::default();
|
||||
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
|
||||
install_output(candidate, dropped.clone());
|
||||
candidate
|
||||
.playback_status
|
||||
.ready
|
||||
.store(false, Ordering::Release);
|
||||
Ok(())
|
||||
});
|
||||
handler.recover_playback_with(Instant::now(), |_, _| {
|
||||
panic!("Startup confirmation deadline has not elapsed")
|
||||
});
|
||||
assert!(!dropped.load(Ordering::SeqCst));
|
||||
let expired = Instant::now() + STARTUP_CONFIRMATION_TIMEOUT;
|
||||
let mut attempts = 0;
|
||||
handler.recover_playback_with(expired, |candidate, requested| {
|
||||
attempts += 1;
|
||||
assert!(dropped.load(Ordering::SeqCst));
|
||||
assert_eq!(requested, format(INPUT_RATE, CHANNELS));
|
||||
install_output(candidate, Arc::new(AtomicBool::new(false)));
|
||||
Ok(())
|
||||
});
|
||||
assert_eq!(attempts, 1);
|
||||
handler.recover_playback_with(expired + STARTUP_CONFIRMATION_TIMEOUT, |_, _| {
|
||||
panic!("Confirmed output must not be reopened")
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn already_terminal_candidate_cannot_replace_compatible_output() {
|
||||
let (mut handler, dropped) = recovery_handler();
|
||||
let candidate_dropped = Arc::new(AtomicBool::new(false));
|
||||
let old_buffer = handler.audio_buffer.0.clone();
|
||||
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
|
||||
install_output(candidate, candidate_dropped.clone());
|
||||
candidate
|
||||
.playback_recovery
|
||||
.errors
|
||||
.push(StreamError::DeviceNotAvailable);
|
||||
Ok(())
|
||||
});
|
||||
assert!(!dropped.load(Ordering::SeqCst));
|
||||
assert!(candidate_dropped.load(Ordering::SeqCst));
|
||||
assert!(Arc::ptr_eq(&old_buffer, &handler.audio_buffer.0));
|
||||
handler.recover_playback_with(Instant::now() + AFTER_COOLDOWN, |_, _| {
|
||||
panic!("Compatible active output must not be reopened")
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_output_keeps_playing_and_transfers_decoder_history_on_commit() {
|
||||
let (mut handler, old_dropped) = recovery_handler();
|
||||
let old_buffer = handler.audio_buffer.0.clone();
|
||||
let (_, status, _) = begin_pending(&mut handler);
|
||||
let frame = audio_frame();
|
||||
let (mut reference, mut expected) = decoder(INPUT_RATE);
|
||||
reference
|
||||
.decode_float(&frame.data, &mut expected, false)
|
||||
.unwrap();
|
||||
handler.handle_frame(frame.clone());
|
||||
assert!(Arc::ptr_eq(&old_buffer, &handler.audio_buffer.0));
|
||||
assert!(!drain_audio(&handler).is_empty());
|
||||
status.ready.store(true, Ordering::Release);
|
||||
handler.recover_playback_with(Instant::now(), |_, _| panic!("Candidate already exists"));
|
||||
assert!(old_dropped.load(Ordering::SeqCst));
|
||||
assert!(Arc::ptr_eq(&status, &handler.playback_status));
|
||||
let samples = reference
|
||||
.decode_float(&frame.data, &mut expected, false)
|
||||
.unwrap()
|
||||
* CHANNELS as usize;
|
||||
handler.handle_frame(frame);
|
||||
assert_eq!(
|
||||
&handler.audio_decoder.as_ref().unwrap().1[..samples],
|
||||
&expected[..samples]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollback_keeps_the_restarted_decoders_accumulated_history() {
|
||||
let (mut handler, old_dropped) = recovery_handler();
|
||||
let frame = audio_frame();
|
||||
handler.handle_frame(frame.clone());
|
||||
let (candidate_dropped, _, errors) = begin_pending(&mut handler);
|
||||
let (mut reference, mut expected) = decoder(INPUT_RATE);
|
||||
handler.handle_frame(frame.clone());
|
||||
reference
|
||||
.decode_float(&frame.data, &mut expected, false)
|
||||
.unwrap();
|
||||
errors.push(StreamError::DeviceNotAvailable);
|
||||
handler.recover_playback_with(Instant::now(), |_, _| panic!("Old output still works"));
|
||||
let samples = reference
|
||||
.decode_float(&frame.data, &mut expected, false)
|
||||
.unwrap()
|
||||
* CHANNELS as usize;
|
||||
handler.handle_frame(frame);
|
||||
assert!(!old_dropped.load(Ordering::SeqCst));
|
||||
assert!(candidate_dropped.load(Ordering::SeqCst));
|
||||
assert_eq!(
|
||||
&handler.audio_decoder.as_ref().unwrap().1[..samples],
|
||||
&expected[..samples]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_compatible_format_retires_only_the_pending_attempt() {
|
||||
let (mut handler, old_dropped) = recovery_handler();
|
||||
let (first_dropped, _, first_errors) = begin_pending(&mut handler);
|
||||
let (second_dropped, second_status, _) = begin_pending(&mut handler);
|
||||
assert!(first_dropped.load(Ordering::SeqCst));
|
||||
assert!(!old_dropped.load(Ordering::SeqCst));
|
||||
first_errors.push(StreamError::DeviceNotAvailable);
|
||||
second_status.ready.store(true, Ordering::Release);
|
||||
handler.recover_playback_with(Instant::now(), |_, _| {
|
||||
panic!("Retired attempt affected current output")
|
||||
});
|
||||
assert!(old_dropped.load(Ordering::SeqCst));
|
||||
assert!(!second_dropped.load(Ordering::SeqCst));
|
||||
assert!(Arc::ptr_eq(&second_status, &handler.playback_status));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_outputs_failing_retains_format_and_paces_recovery() {
|
||||
let (mut handler, old_dropped) = recovery_handler();
|
||||
let old_errors = handler.playback_recovery.errors.clone();
|
||||
let (candidate_dropped, _, errors) = begin_pending(&mut handler);
|
||||
old_errors.push(StreamError::DeviceNotAvailable);
|
||||
errors.push(StreamError::DeviceNotAvailable);
|
||||
handler.recover_playback_with(Instant::now(), |_, _| panic!("Retry must be paced"));
|
||||
assert!(old_dropped.load(Ordering::SeqCst));
|
||||
assert!(candidate_dropped.load(Ordering::SeqCst));
|
||||
assert!(handler.audio_stream.is_none());
|
||||
let due = handler.playback_recovery.retry_at.unwrap();
|
||||
let mut attempts = 0;
|
||||
handler.recover_playback_with(due, |_, requested| {
|
||||
attempts += 1;
|
||||
assert_eq!(requested, format(INPUT_RATE, CHANNELS));
|
||||
Ok(())
|
||||
});
|
||||
assert_eq!(attempts, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn superseding_format_keeps_ready_candidate_when_active_output_failed() {
|
||||
let (mut handler, old_dropped) = recovery_handler();
|
||||
let old_errors = handler.playback_recovery.errors.clone();
|
||||
let (candidate_dropped, candidate_status, _) = begin_pending(&mut handler);
|
||||
candidate_status.ready.store(true, Ordering::Release);
|
||||
old_errors.push(StreamError::DeviceNotAvailable);
|
||||
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), failed_output);
|
||||
assert!(!candidate_dropped.load(Ordering::SeqCst));
|
||||
assert!(old_dropped.load(Ordering::SeqCst));
|
||||
assert!(Arc::ptr_eq(&candidate_status, &handler.playback_status));
|
||||
assert!(handler.audio_decoder.is_some());
|
||||
assert!(handler.playback_recovery.retry_at.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn superseding_format_preserves_failure_when_both_outputs_failed() {
|
||||
let (mut handler, old_dropped) = recovery_handler();
|
||||
let old_errors = handler.playback_recovery.errors.clone();
|
||||
let (candidate_dropped, status, errors) = begin_pending(&mut handler);
|
||||
status.ready.store(true, Ordering::Release);
|
||||
old_errors.push(StreamError::DeviceNotAvailable);
|
||||
errors.push(StreamError::DeviceNotAvailable);
|
||||
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), failed_output);
|
||||
assert!(candidate_dropped.load(Ordering::SeqCst));
|
||||
assert!(old_dropped.load(Ordering::SeqCst));
|
||||
assert!(handler.audio_stream.is_none());
|
||||
assert!(handler.playback_recovery.retry_at.is_some());
|
||||
}
|
||||
128
src/client/tests/audio_state_tests.rs
Normal file
128
src/client/tests/audio_state_tests.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use super::{create_audio_resampler, AudioDecoder, AudioFormat, AudioFrame, AudioHandler, Stereo};
|
||||
use cpal::traits::StreamTrait;
|
||||
use hbb_common::{anyhow::anyhow, ResultType};
|
||||
use magnum_opus::{Application::LowDelay, Encoder};
|
||||
use ringbuf::Rb;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
const INPUT_RATE: u32 = 24_000;
|
||||
const OUTPUT_RATE: u32 = 48_000;
|
||||
const CHANNELS: u16 = 2;
|
||||
const PACKETS_PER_SECOND: usize = 100;
|
||||
const MAX_PACKET_BYTES: usize = 4_096;
|
||||
const SAMPLE_VALUE: f32 = 0.25;
|
||||
const MONO_CHANNELS: u16 = 1;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "audio_playback_recovery_tests.rs"]
|
||||
mod recovery_tests;
|
||||
|
||||
struct TrackedAudioStream(Arc<AtomicBool>);
|
||||
|
||||
impl StreamTrait for TrackedAudioStream {
|
||||
fn play(&self) -> Result<(), cpal::PlayStreamError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<(), cpal::PauseStreamError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TrackedAudioStream {
|
||||
fn drop(&mut self) {
|
||||
self.0.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
fn decoder(sample_rate: u32) -> (AudioDecoder, Vec<f32>) {
|
||||
(
|
||||
AudioDecoder::new(sample_rate, Stereo).unwrap(),
|
||||
vec![0.0; sample_rate as usize * CHANNELS as usize],
|
||||
)
|
||||
}
|
||||
|
||||
fn active_handler(input_rate: u32) -> (AudioHandler, Arc<AtomicBool>) {
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
let handler = AudioHandler {
|
||||
audio_decoder: Some(decoder(input_rate)),
|
||||
audio_resampler: create_audio_resampler(input_rate, OUTPUT_RATE, CHANNELS).unwrap(),
|
||||
sample_rate: (input_rate, OUTPUT_RATE),
|
||||
audio_stream: Some(Box::new(TrackedAudioStream(dropped.clone()))),
|
||||
channels: CHANNELS,
|
||||
device_channel: CHANNELS,
|
||||
..Default::default()
|
||||
};
|
||||
handler.playback_status.ready.store(true, Ordering::Release);
|
||||
(handler, dropped)
|
||||
}
|
||||
|
||||
fn audio_frame() -> AudioFrame {
|
||||
let samples = OUTPUT_RATE as usize / PACKETS_PER_SECOND * CHANNELS as usize;
|
||||
let mut encoder = Encoder::new(OUTPUT_RATE, Stereo, LowDelay).unwrap();
|
||||
AudioFrame {
|
||||
data: encoder
|
||||
.encode_vec_float(&vec![SAMPLE_VALUE; samples], MAX_PACKET_BYTES)
|
||||
.unwrap()
|
||||
.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn failed_output(candidate: &mut AudioHandler, _: AudioFormat) -> ResultType<()> {
|
||||
candidate.sample_rate = (INPUT_RATE, INPUT_RATE);
|
||||
candidate.device_channel = MONO_CHANNELS;
|
||||
candidate
|
||||
.audio_buffer
|
||||
.resize(INPUT_RATE as _, MONO_CHANNELS as _);
|
||||
candidate
|
||||
.playback_status
|
||||
.ready
|
||||
.store(false, Ordering::Release);
|
||||
Err(anyhow!("Injected playback failure"))
|
||||
}
|
||||
|
||||
fn format(sample_rate: u32, channels: u16) -> AudioFormat {
|
||||
AudioFormat {
|
||||
sample_rate,
|
||||
channels: u32::from(channels),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identical_format_failure_preserves_playback_and_resampler_history() {
|
||||
let (mut handler, dropped) = active_handler(INPUT_RATE);
|
||||
let (mut reference, _) = active_handler(INPUT_RATE);
|
||||
let (mut retained_decoder, _) = active_handler(INPUT_RATE);
|
||||
retained_decoder.handle_frame(audio_frame());
|
||||
retained_decoder.handle_frame(audio_frame());
|
||||
handler.handle_frame(audio_frame());
|
||||
reference.handle_frame(audio_frame());
|
||||
let buffer = handler.audio_buffer.0.clone();
|
||||
let status = handler.playback_status.clone();
|
||||
|
||||
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), failed_output);
|
||||
reference.audio_decoder = Some(decoder(INPUT_RATE));
|
||||
handler.handle_frame(audio_frame());
|
||||
reference.handle_frame(audio_frame());
|
||||
|
||||
assert!(!dropped.load(Ordering::SeqCst));
|
||||
assert!(Arc::ptr_eq(&buffer, &handler.audio_buffer.0));
|
||||
assert!(Arc::ptr_eq(&status, &handler.playback_status));
|
||||
assert_eq!(handler.sample_rate, (INPUT_RATE, OUTPUT_RATE));
|
||||
assert_eq!(handler.device_channel, CHANNELS);
|
||||
assert!(handler.playback_status.ready.load(Ordering::Acquire));
|
||||
let expected = drain_audio(&reference);
|
||||
let actual = drain_audio(&handler);
|
||||
assert!(!actual.is_empty());
|
||||
assert_ne!(drain_audio(&retained_decoder), expected);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
fn drain_audio(handler: &AudioHandler) -> Vec<f32> {
|
||||
handler.audio_buffer.0.lock().unwrap().pop_iter().collect()
|
||||
}
|
||||
@@ -408,6 +408,11 @@ pub fn resample_channels(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "use_dasp", feature = "use_samplerate"))]
|
||||
compile_error!(
|
||||
"features `use_dasp` and `use_samplerate` are mutually exclusive; disable default features before selecting `use_samplerate`"
|
||||
);
|
||||
|
||||
#[cfg(feature = "use_dasp")]
|
||||
pub fn audio_resample(
|
||||
data: &[f32],
|
||||
@@ -444,7 +449,7 @@ pub fn audio_resample(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "use_samplerate")]
|
||||
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
|
||||
pub fn audio_resample(
|
||||
data: &[f32],
|
||||
sample_rate0: u32,
|
||||
@@ -2161,6 +2166,13 @@ pub fn decode_id_pk_dtls(
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the DTLS fingerprint a WebRTC peer signed into its identity is the one of the channel
|
||||
/// actually negotiated. An empty signed value binds nothing: on a WebRTC channel it is either a
|
||||
/// peer that could not sign one or a rendezvous/relay that stripped it, and both fail closed.
|
||||
pub fn dtls_fingerprint_bound(signed_fp: &str, actual_fp: &str) -> bool {
|
||||
!signed_fp.is_empty() && signed_fp == actual_fp
|
||||
}
|
||||
|
||||
pub fn create_symmetric_key_msg(their_pk_b: [u8; 32]) -> (Bytes, Bytes, secretbox::Key) {
|
||||
let their_pk_b = box_::PublicKey(their_pk_b);
|
||||
let (our_pk_b, out_sk_b) = box_::gen_keypair();
|
||||
@@ -3258,4 +3270,42 @@ mod tests {
|
||||
assert_eq!(combined_mask & MOUSE_TYPE_MASK, MOUSE_TYPE_DOWN);
|
||||
assert_eq!(combined_mask >> 3, MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dtls_fingerprint_travels_signed_and_binds() {
|
||||
let (pk, sk) = sign::gen_keypair();
|
||||
let fp = "sha-256 0A:1B:2C";
|
||||
let signed = sign::sign(
|
||||
&IdPk {
|
||||
id: "123456789".to_owned(),
|
||||
pk: Bytes::from(vec![7u8; 32]),
|
||||
dtls_fingerprint: fp.to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
.write_to_bytes()
|
||||
.unwrap(),
|
||||
&sk,
|
||||
);
|
||||
|
||||
let (id, their_pk, signed_fp) = decode_id_pk_dtls(&signed, &pk).unwrap();
|
||||
assert_eq!(id, "123456789");
|
||||
assert_eq!(their_pk, [7u8; 32]);
|
||||
assert_eq!(signed_fp, fp);
|
||||
assert!(dtls_fingerprint_bound(&signed_fp, fp));
|
||||
assert!(!dtls_fingerprint_bound(&signed_fp, "sha-256 0A:1B:2D"));
|
||||
assert!(!dtls_fingerprint_bound("", ""));
|
||||
|
||||
// The fingerprint is under the signature: a blob verified with another key yields
|
||||
// nothing, and one whose payload was edited in transit fails verification.
|
||||
let (other_pk, _) = sign::gen_keypair();
|
||||
assert!(decode_id_pk_dtls(&signed, &other_pk).is_err());
|
||||
let mut tampered = signed.clone();
|
||||
let last = tampered.len() - 1;
|
||||
tampered[last] ^= 1;
|
||||
assert!(decode_id_pk_dtls(&tampered, &pk).is_err());
|
||||
|
||||
// `decode_id_pk` is the same blob minus the fingerprint, so the field is invisible to
|
||||
// non-WebRTC handshakes.
|
||||
assert_eq!(decode_id_pk(&signed, &pk).unwrap(), (id, their_pk));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use hbb_common::{
|
||||
bail,
|
||||
base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _},
|
||||
base64::{
|
||||
engine::general_purpose::{URL_SAFE, URL_SAFE_NO_PAD},
|
||||
Engine as _,
|
||||
},
|
||||
sodiumoxide::crypto::sign,
|
||||
ResultType,
|
||||
};
|
||||
@@ -25,7 +28,9 @@ fn get_custom_server_from_config_string(s: &str) -> ResultType<CustomServer> {
|
||||
12, 46, 129, 83, 17, 84, 193, 119, 197, 130, 103,
|
||||
];
|
||||
let pk = sign::PublicKey(*PK);
|
||||
let data = URL_SAFE_NO_PAD.decode(tmp)?;
|
||||
let data = URL_SAFE_NO_PAD
|
||||
.decode(&tmp)
|
||||
.or_else(|_| URL_SAFE.decode(&tmp))?;
|
||||
if let Ok(lic) = serde_json::from_slice::<CustomServer>(&data) {
|
||||
return Ok(lic);
|
||||
}
|
||||
@@ -215,5 +220,16 @@ mod test {
|
||||
assert_eq!(
|
||||
get_custom_server_from_string("rustdesk-licensed--0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye--.exe")
|
||||
.unwrap(), lic);
|
||||
// padded base64 (one '=' after reversal) is accepted, wrong padding is not
|
||||
assert_eq!(
|
||||
get_custom_server_from_string("rustdesk-licensed-=0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye.exe")
|
||||
.unwrap(), lic);
|
||||
assert!(
|
||||
get_custom_server_from_string("rustdesk-licensed-==0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye.exe")
|
||||
.is_err());
|
||||
// bare string as passed to `--config`
|
||||
assert_eq!(
|
||||
get_custom_server_from_string("=0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye.exe")
|
||||
.unwrap(), lic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1497,7 +1497,13 @@ pub async fn start_pa() {
|
||||
None, // Use default buffering attributes
|
||||
) {
|
||||
Ok(s) => loop {
|
||||
if let Ok(_) = s.read(&mut buf) {
|
||||
// A dead pulse handle fails every read at once, so ignoring the
|
||||
// error left nothing pacing this loop and it burned a core.
|
||||
if let Err(err) = s.read(&mut buf) {
|
||||
log::error!("Failed to read audio data:{}", err);
|
||||
break;
|
||||
}
|
||||
{
|
||||
let out =
|
||||
if buf.iter().filter(|x| **x != 0).next().is_none() {
|
||||
vec![]
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "انتهى طلب مشاركة الشاشة على الجهاز البعيد دون أن يكتمل"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "تعذّر على RustDesk الحصول على شاشة قابلة للاستخدام من XDG Desktop Portal، قد تكون مكتبة PipeWire قديمة جدًا"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "تعذّر على RustDesk تحميل مكوّن GStreamer اللازم لالتقاط الشاشة ({})"),
|
||||
("Relay fallback delay in seconds", "مهلة التراجع إلى الترحيل بالثواني"),
|
||||
("relay-fallback-delay-tip", "المدة التي ينتظرها اتصال الترحيل القائم بالفعل الاتصالَ المباشر عبر WebRTC قبل أن يُستخدم بدلًا منه. زِدها لمنح الاتصال المباشر البطيء فرصة أكبر للفوز؛ وقلّلها للاستقرار على الترحيل أسرع في الشبكات التي يتعذر فيها الاتصال المباشر. اتركها فارغة للقيمة الافتراضية 2.5 ثانية."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -770,5 +770,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("port-forward-mux-tip", "Port yönləndirmə xəritələnməsinin hər əlaqəsini qarşı tərəfə açılan tək əlaqə üzərindən daşıyır, hər biri üçün yenidən qoşulub giriş etmək əvəzinə."),
|
||||
("Enable WebRTC P2P connection", "WebRTC P2P əlaqəsini aktivləşdir"),
|
||||
("Enable TCP hole punching", "TCP deşik açmanı aktivləşdir"),
|
||||
("The screen sharing request was declined on the remote device", ""),
|
||||
("The screen sharing request timed out on the remote device", ""),
|
||||
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", ""),
|
||||
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", ""),
|
||||
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", ""),
|
||||
("The screen sharing request ended without completing on the remote device", ""),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", ""),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", ""),
|
||||
("Relay fallback delay in seconds", "Ötürücüyə keçid gecikməsi, saniyə"),
|
||||
("relay-fallback-delay-tip", "Artıq qurulmuş ötürücü bağlantı birbaşa WebRTC bağlantısını nə qədər gözləyir, sonra onun əvəzinə istifadə olunur. Yavaş birbaşa bağlantıya daha çox vaxt vermək üçün artırın; birbaşa bağlantının mümkün olmadığı şəbəkələrdə ötürücüyə daha tez keçmək üçün azaldın. Standart 2.5 saniyə üçün boş buraxın."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Запыт на абагульванне экрана на аддаленай прыладзе завяршыўся, не будучы выкананым"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk не змог атрымаць прыдатны экран ад XDG Desktop Portal, магчыма бібліятэка PipeWire занадта старая"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не змог загрузіць кампанент GStreamer, патрэбны для захопу экрана ({})"),
|
||||
("Relay fallback delay in seconds", "Затрымка пераходу на рэтранслятар у секундах"),
|
||||
("relay-fallback-delay-tip", "Колькі часу ўжо ўсталяванае злучэнне праз рэтранслятар чакае прамога злучэння WebRTC, перш чым будзе выкарыстана замест яго. Павялічце, каб даць павольнаму прамому злучэнню больш часу; паменшыце, каб хутчэй пераходзіць на рэтранслятар у сетках, дзе прамое злучэнне немагчымае. Пакіньце пустым для значэння па змаўчанні 2.5 секунды."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Заявката за споделяне на екрана на отдалеченото устройство приключи, без да бъде изпълнена"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk не можа да получи използваем екран от XDG Desktop Portal, библиотеката PipeWire може да е твърде стара"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не можа да зареди компонент на GStreamer, необходим за заснемане на екрана ({})"),
|
||||
("Relay fallback delay in seconds", "Забавяне преди преминаване към препредаване в секунди"),
|
||||
("relay-fallback-delay-tip", "Колко време вече установената връзка чрез препредаване изчаква директната WebRTC връзка, преди да бъде използвана вместо нея. Увеличете, за да дадете повече време на бавна директна връзка; намалете, за да се премине по-бързо към препредаване в мрежи, където директна връзка е невъзможна. Оставете празно за стойността по подразбиране 2.5 секунди."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "La sol·licitud de compartició de pantalla al dispositiu remot ha acabat sense completar-se"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "El RustDesk no ha pogut obtenir cap pantalla utilitzable de l'XDG Desktop Portal; la biblioteca PipeWire pot ser massa antiga"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "El RustDesk no ha pogut carregar un component del GStreamer necessari per capturar la pantalla ({})"),
|
||||
("Relay fallback delay in seconds", "Retard abans de recórrer al relé en segons"),
|
||||
("relay-fallback-delay-tip", "Quant de temps espera una connexió de relé ja establerta la connexió directa WebRTC abans d'utilitzar-se en lloc seu. Augmenteu-lo per donar més temps a una connexió directa lenta; reduïu-lo per passar abans al relé en xarxes on no es pot fer una connexió directa. Deixeu-lo buit per al valor predeterminat de 2.5 segons."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "远程设备上的屏幕共享请求已结束,但未完成"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk 无法从 XDG Desktop Portal 获取可用的屏幕,PipeWire 库可能过旧"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk 无法加载屏幕捕获所需的 GStreamer 组件 ({})"),
|
||||
("Relay fallback delay in seconds", "回落到中继前的等待时间(秒)"),
|
||||
("relay-fallback-delay-tip", "已经建立的中继连接会等待直连的 WebRTC 多久,超过这个时间就改用中继。调大可以让较慢的直连有更多机会胜出;调小则在无法直连的网络上更快回落到中继。留空表示使用默认值 2.5 秒。"),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Žádost o sdílení obrazovky na vzdáleném zařízení skončila, aniž by byla dokončena"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nezískal z XDG Desktop Portal použitelnou obrazovku, knihovna PipeWire může být příliš stará"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nemohl načíst komponentu GStreameru potřebnou k zachycení obrazovky ({})"),
|
||||
("Relay fallback delay in seconds", "Prodleva před přepnutím na přenos v sekundách"),
|
||||
("relay-fallback-delay-tip", "Jak dlouho již navázané spojení přes přenos čeká na přímé spojení WebRTC, než bude použito místo něj. Zvyšte, aby pomalé přímé spojení mělo více času uspět; snižte, aby se v sítích, kde přímé spojení není možné, dříve přešlo na přenos. Ponechte prázdné pro výchozí hodnotu 2.5 sekundy."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Anmodningen om skærmdeling på fjernenheden sluttede uden at blive gennemført"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk kunne ikke få en brugbar skærm fra XDG Desktop Portal, PipeWire-biblioteket er måske for gammelt"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kunne ikke indlæse en GStreamer-komponent, der kræves til skærmoptagelse ({})"),
|
||||
("Relay fallback delay in seconds", "Forsinkelse før brug af relæ i sekunder"),
|
||||
("relay-fallback-delay-tip", "Hvor længe en allerede oprettet relæforbindelse venter på den direkte WebRTC-forbindelse, før den bruges i stedet. Forøg for at give en langsom direkte forbindelse mere tid; sænk for hurtigere at falde tilbage til relæet på netværk, hvor en direkte forbindelse ikke kan oprettes. Lad feltet stå tomt for standardværdien 2.5 sekunder."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Die Anfrage zur Bildschirmfreigabe auf dem entfernten Gerät endete, ohne abgeschlossen zu werden"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk konnte vom XDG Desktop Portal keinen nutzbaren Bildschirm erhalten, die PipeWire-Bibliothek ist möglicherweise zu alt"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk konnte eine für die Bildschirmaufnahme benötigte GStreamer-Komponente nicht laden ({})"),
|
||||
("Relay fallback delay in seconds", "Verzögerung bis zum Relais in Sekunden"),
|
||||
("relay-fallback-delay-tip", "Wie lange eine bereits aufgebaute Relaisverbindung auf die direkte WebRTC-Verbindung wartet, bevor sie stattdessen verwendet wird. Erhöhen Sie den Wert, um einer langsamen direkten Verbindung mehr Zeit zu geben; verringern Sie ihn, um in Netzwerken ohne mögliche Direktverbindung schneller auf das Relais zurückzufallen. Leer lassen für den Standardwert von 2.5 Sekunden."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Το αίτημα κοινής χρήσης οθόνης στην απομακρυσμένη συσκευή έληξε χωρίς να ολοκληρωθεί"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "Το RustDesk δεν μπόρεσε να λάβει αξιοποιήσιμη οθόνη από το XDG Desktop Portal, η βιβλιοθήκη PipeWire ίσως είναι πολύ παλιά"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "Το RustDesk δεν μπόρεσε να φορτώσει ένα στοιχείο του GStreamer που απαιτείται για την καταγραφή οθόνης ({})"),
|
||||
("Relay fallback delay in seconds", "Καθυστέρηση πριν από τη χρήση αναμεταδότη σε δευτερόλεπτα"),
|
||||
("relay-fallback-delay-tip", "Πόσο χρόνο περιμένει μια ήδη ενεργή σύνδεση αναμεταδότη την απευθείας σύνδεση WebRTC πριν χρησιμοποιηθεί στη θέση της. Αυξήστε το για να δώσετε σε μια αργή απευθείας σύνδεση περισσότερο χρόνο. Μειώστε το για ταχύτερη επιστροφή στον αναμεταδότη σε δίκτυα όπου δεν είναι δυνατή η απευθείας σύνδεση. Αφήστε το κενό για την προεπιλογή των 2.5 δευτερολέπτων."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -84,10 +84,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("android_input_permission_tip1", "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."),
|
||||
("android_input_permission_tip2", "Please go to the next system settings page, find and enter [Installed Services], turn on [RustDesk Input] service."),
|
||||
("android_new_connection_tip", "New control request has been received, which wants to control your current device."),
|
||||
("android_service_will_start_tip", "Turning on \"Screen Capture\" will automatically start the service, allowing other devices to request a connection to your device."),
|
||||
("android_service_will_start_tip", "Turning on \"Screen capture\" will automatically start the service, allowing other devices to request a connection to your device."),
|
||||
("android_stop_service_tip", "Closing the service will automatically close all established connections."),
|
||||
("android_version_audio_tip", "The current Android version does not support audio capture, please upgrade to Android 10 or higher."),
|
||||
("android_start_service_tip", "Tap [Start service] or enable [Screen Capture] permission to start the screen sharing service."),
|
||||
("android_start_service_tip", "Tap [Start service] or enable [Screen capture] permission to start the screen sharing service."),
|
||||
("android_permission_may_not_change_tip", "Permissions for established connections may not be changed instantly until reconnected."),
|
||||
("doc_mac_permission", "https://rustdesk.com/docs/en/client/mac/#enable-permissions"),
|
||||
("Ignore Battery Optimizations", "Ignore battery optimizations"),
|
||||
@@ -252,11 +252,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("disable-udp-tip", "Controls whether to use TCP only.\nWhen this option enabled, RustDesk will not use UDP 21116 any more, TCP 21116 will be used instead."),
|
||||
("server-oss-not-support-tip", "NOTE: RustDesk server OSS doesn't include this feature."),
|
||||
("note-at-conn-end-tip", "Ask for note at end of connection"),
|
||||
("rel-mouse-not-supported-peer-tip", "Relative Mouse Mode is not supported by the connected peer."),
|
||||
("rel-mouse-not-ready-tip", "Relative Mouse Mode is not ready yet. Please try again."),
|
||||
("rel-mouse-lock-failed-tip", "Failed to lock cursor. Relative Mouse Mode has been disabled."),
|
||||
("rel-mouse-not-supported-peer-tip", "Relative mouse mode is not supported by the connected peer."),
|
||||
("rel-mouse-not-ready-tip", "Relative mouse mode is not ready yet. Please try again."),
|
||||
("rel-mouse-lock-failed-tip", "Failed to lock cursor. Relative mouse mode has been disabled."),
|
||||
("rel-mouse-exit-{}-tip", "Press {} to exit."),
|
||||
("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative Mouse Mode has been disabled."),
|
||||
("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative mouse mode has been disabled."),
|
||||
("keep-awake-during-outgoing-sessions-label", "Keep screen awake during outgoing sessions"),
|
||||
("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"),
|
||||
("password-hidden-tip", "Permanent password is set (hidden)."),
|
||||
@@ -278,5 +278,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("sync-clipboard-between-sessions-tip", "Text or images copied in one remote session are also sent to the clipboard of your other connected sessions."),
|
||||
("terminal-clipboard-write-tip", "An app in the terminal wants to copy text to this device's clipboard. If granted, this permission applies to terminal apps in all connections until you turn it off in Settings. Manual copy and paste are unaffected."),
|
||||
("port-forward-mux-tip", "Carry every connection of a port-forward mapping over a single connection to the peer, instead of connecting and logging in again for each one."),
|
||||
("relay-fallback-delay-tip", "How long a relay connection that is already up waits for the direct WebRTC connection before it is used instead. Raise it to give a slow direct connection more time to win; lower it to settle on the relay sooner on networks where a direct connection cannot be made. Leave empty for the default of 2.5 seconds."),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "La peto pri ekrandividado sur la fora aparato finiĝis sen kompletiĝi"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk ne povis akiri uzeblan ekranon de XDG Desktop Portal, la biblioteko PipeWire eble estas tro malnova"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ne povis ŝargi komponanton de GStreamer necesan por ekrankapto ({})"),
|
||||
("Relay fallback delay in seconds", "Prokrasto antaŭ retransmisio en sekundoj"),
|
||||
("relay-fallback-delay-tip", "Kiom longe jam establita retransmisia konekto atendas la rektan WebRTC-konekton antaŭ ol esti uzata anstataŭe. Pligrandigu ĝin por doni al malrapida rekta konekto pli da tempo; malpligrandigu ĝin por pli frue uzi la retransmision en retoj kie rekta konekto ne eblas. Lasu malplena por la defaŭlta valoro de 2.5 sekundoj."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("android_service_will_start_tip", "Habilitar la captura de pantalla iniciará automáticamente el servicio, lo que permitirá que otros dispositivos soliciten una conexión desde este dispositivo."),
|
||||
("android_stop_service_tip", "Cerrar el servicio cerrará automáticamente todas las conexiones establecidas."),
|
||||
("android_version_audio_tip", "La versión actual de Android no admite la captura de audio, actualice a Android 10 o posterior."),
|
||||
("android_start_service_tip", "Toque [Iniciar servicio] o conceda el permiso [Captura de pantalla] para iniciar el servicio de pantalla compartida."),
|
||||
("android_start_service_tip", "Toque [Iniciar Servicio] o conceda el permiso [Captura de pantalla] para iniciar el servicio de pantalla compartida."),
|
||||
("android_permission_may_not_change_tip", "Es posible que los permisos de las conexiones ya establecidas no cambien de inmediato hasta que se vuelva a conectar."),
|
||||
("Account", "Cuenta"),
|
||||
("Overwrite", "Sobrescribir"),
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "La solicitud de compartir pantalla en el dispositivo remoto terminó sin completarse"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk no ha podido obtener una pantalla utilizable del XDG Desktop Portal; la biblioteca PipeWire puede ser demasiado antigua"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk no ha podido cargar un componente de GStreamer necesario para capturar la pantalla ({})"),
|
||||
("Relay fallback delay in seconds", "Retardo antes de usar el relé en segundos"),
|
||||
("relay-fallback-delay-tip", "Cuánto tiempo espera una conexión de relé ya establecida a la conexión directa WebRTC antes de usarse en su lugar. Auméntelo para dar más tiempo a una conexión directa lenta; redúzcalo para recurrir antes al relé en redes donde no es posible una conexión directa. Déjelo vacío para el valor predeterminado de 2.5 segundos."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Enter your password", "Sisesta oma parool"),
|
||||
("Logging in...", "Sisselogimine..."),
|
||||
("Enable RDP session sharing", "Luba RDP-seansi jagamine"),
|
||||
("Auto Login", "Logi automaatselt sisse (Kehtib vaid valiku \"lukusta pärast seansi lõppu\" lubamisel)"),
|
||||
("Auto Login", "Logi automaatselt sisse (Kehtib vaid valiku \"Lukusta pärast seansi lõppu\" lubamisel)"),
|
||||
("Enable direct IP access", "Luba otsene IP-juurdepääs"),
|
||||
("Rename", "Nimeta ümber"),
|
||||
("Space", "Ruum"),
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Ekraani jagamise taotlus kaugseadmes lõppes ilma lõpule jõudmata"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk ei saanud XDG Desktop Portalilt kasutatavat ekraani, PipeWire'i teek võib olla liiga vana"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ei suutnud laadida ekraani jäädvustamiseks vajalikku GStreameri komponenti ({})"),
|
||||
("Relay fallback delay in seconds", "Viivitus enne relee kasutamist sekundites"),
|
||||
("relay-fallback-delay-tip", "Kui kaua juba loodud releeühendus ootab otsest WebRTC-ühendust, enne kui seda selle asemel kasutatakse. Suurendage, et anda aeglasele otseühendusele rohkem aega; vähendage, et võrkudes, kus otseühendust luua ei saa, releele kiiremini üle minna. Jätke tühjaks vaikeväärtuse 2.5 sekundit kasutamiseks."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Urruneko gailuko pantaila partekatzeko eskaera osatu gabe amaitu da"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk-ek ezin izan du pantaila erabilgarririk lortu XDG Desktop Portal-etik, PipeWire liburutegia zaharregia izan daiteke"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk-ek ezin izan du pantaila kapturatzeko beharrezkoa den GStreamer osagai bat kargatu ({})"),
|
||||
("Relay fallback delay in seconds", "Errelera itzultzeko atzerapena segundotan"),
|
||||
("relay-fallback-delay-tip", "Dagoeneko ezarritako errele-konexio batek WebRTC konexio zuzenari zenbat denbora itxaroten dion, haren ordez erabili aurretik. Handitu konexio zuzen motel bati denbora gehiago emateko; txikitu konexio zuzena egin ezin den sareetan lehenago errelera itzultzeko. Utzi hutsik 2.5 segundoko balio lehenetsirako."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "درخواست اشتراکگذاری صفحه در دستگاه راه دور بدون تکمیل شدن پایان یافت"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk نتوانست صفحهای قابل استفاده از XDG Desktop Portal دریافت کند، ممکن است کتابخانه PipeWire خیلی قدیمی باشد"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk نتوانست مؤلفه GStreamer موردنیاز برای ضبط صفحه را بارگذاری کند ({})"),
|
||||
("Relay fallback delay in seconds", "تأخیر بازگشت به رله بر حسب ثانیه"),
|
||||
("relay-fallback-delay-tip", "یک اتصال رله که از قبل برقرار شده چقدر منتظر اتصال مستقیم WebRTC می ماند پیش از آنکه به جای آن استفاده شود. آن را افزایش دهید تا به اتصال مستقیم کند فرصت بیشتری داده شود؛ کاهش دهید تا در شبکه هایی که اتصال مستقیم ممکن نیست، زودتر به رله بازگردد. برای مقدار پیش فرض 2.5 ثانیه خالی بگذارید."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Näytön jakamispyyntö etälaitteessa päättyi ilman että se saatiin valmiiksi"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk ei saanut XDG Desktop Portalilta käyttökelpoista näyttöä, PipeWire-kirjasto voi olla liian vanha"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ei voinut ladata näytön kaappaukseen tarvittavaa GStreamer-osaa ({})"),
|
||||
("Relay fallback delay in seconds", "Viive ennen välitykseen siirtymistä sekunteina"),
|
||||
("relay-fallback-delay-tip", "Kuinka kauan jo muodostettu välitysyhteys odottaa suoraa WebRTC-yhteyttä ennen kuin sitä käytetään sen sijaan. Kasvata arvoa antaaksesi hitaalle suoralle yhteydelle enemmän aikaa; pienennä sitä siirtyäksesi nopeammin välitykseen verkoissa, joissa suoraa yhteyttä ei voi muodostaa. Jätä tyhjäksi käyttääksesi oletusarvoa 2.5 sekuntia."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "La demande de partage d'écran sur l'appareil distant s'est terminée sans aboutir"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk n'a pas pu obtenir d'écran exploitable auprès du XDG Desktop Portal, la bibliothèque PipeWire est peut-être trop ancienne"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk n'a pas pu charger un composant GStreamer nécessaire à la capture d'écran ({})"),
|
||||
("Relay fallback delay in seconds", "Délai avant bascule vers le relais en secondes"),
|
||||
("relay-fallback-delay-tip", "Durée pendant laquelle une connexion relais déjà établie attend la connexion directe WebRTC avant d'être utilisée à sa place. Augmentez-la pour laisser plus de temps à une connexion directe lente ; diminuez-la pour basculer plus tôt vers le relais sur les réseaux où une connexion directe est impossible. Laissez vide pour la valeur par défaut de 2.5 secondes."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "ეკრანის გაზიარების მოთხოვნა დისტანციურ მოწყობილობაზე დასრულდა შეუსრულებლად"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk-მა ვერ მიიღო გამოსადეგი ეკრანი XDG Desktop Portal-იდან, PipeWire-ის ბიბლიოთეკა შესაძლოა ძალიან ძველია"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk-მა ვერ ჩატვირთა ეკრანის ჩაწერისთვის საჭირო GStreamer-ის კომპონენტი ({})"),
|
||||
("Relay fallback delay in seconds", "რელეზე გადასვლის დაყოვნება წამებში"),
|
||||
("relay-fallback-delay-tip", "რამდენ ხანს ელოდება უკვე დამყარებული რელე-კავშირი პირდაპირ WebRTC კავშირს, სანამ მის ნაცვლად გამოიყენება. გაზარდეთ, რომ ნელ პირდაპირ კავშირს მეტი დრო მისცეთ; შეამცირეთ, რომ ქსელებში, სადაც პირდაპირი კავშირი შეუძლებელია, უფრო სწრაფად გადავიდეს რელეზე. დატოვეთ ცარიელი ნაგულისხმევი 2.5 წამისთვის."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "A solicitude de compartir pantalla no dispositivo remoto rematou sen completarse"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk non puido obter unha pantalla utilizable do XDG Desktop Portal, a biblioteca PipeWire pode ser demasiado antiga"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk non puido cargar un compoñente de GStreamer necesario para capturar a pantalla ({})"),
|
||||
("Relay fallback delay in seconds", "Atraso antes de usar o relé en segundos"),
|
||||
("relay-fallback-delay-tip", "Canto tempo agarda unha conexión de relé xa establecida pola conexión directa WebRTC antes de usarse no seu lugar. Auménteo para darlle máis tempo a unha conexión directa lenta; redúzao para recorrer antes ao relé en redes onde non é posible unha conexión directa. Déixeo baleiro para o valor predeterminado de 2.5 segundos."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "રિમોટ ઉપકરણ પર સ્ક્રીન શેરિંગ વિનંતી પૂર્ણ થયા વિના સમાપ્ત થઈ"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk XDG Desktop Portal પાસેથી ઉપયોગી સ્ક્રીન મેળવી શક્યું નથી, PipeWire લાઇબ્રેરી કદાચ ઘણી જૂની છે"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk સ્ક્રીન કૅપ્ચર માટે જરૂરી GStreamer ઘટક લોડ કરી શક્યું નથી ({})"),
|
||||
("Relay fallback delay in seconds", "રિલે પર પાછા ફરવામાં વિલંબ સેકન્ડમાં"),
|
||||
("relay-fallback-delay-tip", "પહેલેથી સ્થાપિત રિલે કનેક્શન સીધા WebRTC કનેક્શનની કેટલો સમય રાહ જુએ છે, ત્યાર બાદ તેના બદલે વપરાય છે. ધીમા સીધા કનેક્શનને વધુ સમય આપવા માટે વધારો; જ્યાં સીધું કનેક્શન શક્ય નથી તેવા નેટવર્ક પર વહેલા રિલે પર જવા માટે ઘટાડો. મૂળભૂત 2.5 સેકન્ડ માટે ખાલી રાખો."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "בקשת שיתוף המסך במכשיר המרוחק הסתיימה מבלי להתבצע"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk לא הצליח לקבל מסך שמיש מ-XDG Desktop Portal, ייתכן שספריית PipeWire ישנה מדי"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk לא הצליח לטעון רכיב GStreamer הדרוש ללכידת מסך ({})"),
|
||||
("Relay fallback delay in seconds", "השהיה לפני מעבר לממסר בשניות"),
|
||||
("relay-fallback-delay-tip", "כמה זמן חיבור ממסר שכבר נוצר ממתין לחיבור WebRTC הישיר לפני שישמש במקומו. הגדל כדי לתת לחיבור ישיר איטי יותר זמן; הקטן כדי לעבור מהר יותר לממסר ברשתות שבהן לא ניתן ליצור חיבור ישיר. השאר ריק לערך ברירת המחדל של 2.5 שניות."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "रिमोट डिवाइस पर स्क्रीन शेयरिंग अनुरोध पूरा हुए बिना समाप्त हो गया"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk XDG Desktop Portal से उपयोग योग्य स्क्रीन प्राप्त नहीं कर सका, PipeWire लाइब्रेरी बहुत पुरानी हो सकती है"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk स्क्रीन कैप्चर के लिए आवश्यक GStreamer घटक लोड नहीं कर सका ({})"),
|
||||
("Relay fallback delay in seconds", "रिले पर लौटने में विलंब सेकंड में"),
|
||||
("relay-fallback-delay-tip", "पहले से स्थापित रिले कनेक्शन सीधे WebRTC कनेक्शन की कितनी देर प्रतीक्षा करता है, उसके बाद उसके स्थान पर उपयोग किया जाता है। धीमे सीधे कनेक्शन को अधिक समय देने के लिए बढ़ाएँ; जिन नेटवर्क पर सीधा कनेक्शन संभव नहीं है वहाँ जल्दी रिले पर जाने के लिए घटाएँ। डिफ़ॉल्ट 2.5 सेकंड के लिए खाली छोड़ें।"),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Zahtjev za dijeljenje zaslona na udaljenom uređaju završio je bez dovršetka"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nije mogao dobiti upotrebljiv zaslon od XDG Desktop Portala, PipeWire biblioteka je možda prestara"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nije mogao učitati GStreamer komponentu potrebnu za snimanje zaslona ({})"),
|
||||
("Relay fallback delay in seconds", "Odgoda prije prelaska na relej u sekundama"),
|
||||
("relay-fallback-delay-tip", "Koliko dugo već uspostavljena relejna veza čeka izravnu WebRTC vezu prije nego što se upotrijebi umjesto nje. Povećajte da sporoj izravnoj vezi date više vremena; smanjite da se na mrežama gdje izravna veza nije moguća brže prijeđe na relej. Ostavite prazno za zadanu vrijednost od 2.5 sekunde."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "A képernyőmegosztási kérés a távoli eszközön befejeződött anélkül, hogy teljesült volna"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "A RustDesk nem kapott használható képernyőt az XDG Desktop Portaltól, a PipeWire programkönyvtár túl régi lehet"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "A RustDesk nem tudta betölteni a képernyőrögzítéshez szükséges GStreamer összetevőt ({})"),
|
||||
("Relay fallback delay in seconds", "Késleltetés a továbbítóra váltás előtt másodpercben"),
|
||||
("relay-fallback-delay-tip", "Mennyi ideig vár a már létrejött továbbító kapcsolat a közvetlen WebRTC kapcsolatra, mielőtt helyette használnák. Növelje, hogy a lassú közvetlen kapcsolatnak több ideje legyen; csökkentse, hogy olyan hálózatokon, ahol közvetlen kapcsolat nem hozható létre, hamarabb váltson továbbítóra. Hagyja üresen az alapértelmezett 2.5 másodperchez."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -716,11 +716,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("note-at-conn-end-tip", "Minta catatan di akhir koneksi"),
|
||||
("Show terminal extra keys", "Tampilkan tombol tambahan terminal"),
|
||||
("Relative mouse mode", "Mode mouse relatif"),
|
||||
("rel-mouse-not-supported-peer-tip", "Mode Mouse Relatif tidak didukung oleh peer yang terhubung."),
|
||||
("rel-mouse-not-ready-tip", "Mode Mouse Relatif belum siap. Silakan coba lagi."),
|
||||
("rel-mouse-lock-failed-tip", "Gagal mengunci kursor. Mode Mouse Relatif telah dinonaktifkan."),
|
||||
("rel-mouse-not-supported-peer-tip", "Mode mouse relatif tidak didukung oleh peer yang terhubung."),
|
||||
("rel-mouse-not-ready-tip", "Mode mouse relatif belum siap. Silakan coba lagi."),
|
||||
("rel-mouse-lock-failed-tip", "Gagal mengunci kursor. Mode mouse relatif telah dinonaktifkan."),
|
||||
("rel-mouse-exit-{}-tip", "Tekan {} untuk keluar."),
|
||||
("rel-mouse-permission-lost-tip", "Izin keyboard dicabut. Mode Mouse Relatif telah dinonaktifkan."),
|
||||
("rel-mouse-permission-lost-tip", "Izin keyboard dicabut. Mode mouse relatif telah dinonaktifkan."),
|
||||
("Changelog", "Catatan perubahan"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Jaga layar tetap menyala selama sesi keluar"),
|
||||
("keep-awake-during-incoming-sessions-label", "Jaga layar tetap menyala selama sesi masuk"),
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Permintaan berbagi layar di perangkat jarak jauh berakhir tanpa diselesaikan"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk tidak mendapatkan layar yang dapat digunakan dari XDG Desktop Portal, pustaka PipeWire mungkin terlalu lama"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk tidak dapat memuat komponen GStreamer yang diperlukan untuk merekam layar ({})"),
|
||||
("Relay fallback delay in seconds", "Jeda sebelum beralih ke relai dalam detik"),
|
||||
("relay-fallback-delay-tip", "Berapa lama koneksi relai yang sudah terbentuk menunggu koneksi langsung WebRTC sebelum digunakan sebagai gantinya. Perbesar untuk memberi koneksi langsung yang lambat lebih banyak waktu; perkecil agar lebih cepat beralih ke relai pada jaringan yang tidak memungkinkan koneksi langsung. Biarkan kosong untuk nilai bawaan 2.5 detik."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -541,7 +541,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Plug out all", "Scollega tutto"),
|
||||
("True color (4:4:4)", "Colore reale (4:4:4)"),
|
||||
("Enable blocking user input", "Abilita blocco input utente"),
|
||||
("id_input_tip", "Puoi inserire un ID, un IP diretto o un dominio con una porta (<dominio>:<porta>).\nSe vuoi accedere as un dispositivo in un altro server, aggiungi l'indirizzo del server (<id>@<indirizzo_server >?key=<valore_chiave>), ad esempio\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere as un dispositivo in un server pubblico, inserisci \"<id>@public\", per il server pubblico la chiave non è necessaria\n\nSe vuoi forzare l'uso di una connessione di inoltro alla prima connessione, aggiungi \"/r\" alla fine dell'ID, ad esempio \"9123456234/r\"."),
|
||||
("id_input_tip", "Puoi inserire un ID, un IP diretto o un dominio con una porta (<dominio>:<porta>).\nSe vuoi accedere a un dispositivo in un altro server, aggiungi l'indirizzo del server (<id>@<indirizzo_server >?key=<valore_chiave>), ad esempio\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere as un dispositivo in un server pubblico, inserisci \"<id>@public\", per il server pubblico la chiave non è necessaria.\n\nSe vuoi forzare l'uso di una connessione di inoltro alla prima connessione, aggiungi \"/r\" alla fine dell'ID, ad esempio \"9123456234/r\"."),
|
||||
("privacy_mode_impl_mag_tip", "Modo 1"),
|
||||
("privacy_mode_impl_virtual_display_tip", "Modo 2"),
|
||||
("Enter privacy mode", "Entra in modalità privacy"),
|
||||
@@ -622,7 +622,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Parent directory", "Cartella principale"),
|
||||
("Resume", "Riprendi"),
|
||||
("Invalid file name", "Nome file non valido"),
|
||||
("one-way-file-transfer-tip", "Sul lato controllato è abilitato il trasferimento file unidirezionale."),
|
||||
("one-way-file-transfer-tip", "Sul lato controllato è abilitato il trasferimento file unidirezionale."),
|
||||
("Authentication Required", "Richiesta autenticazione"),
|
||||
("Authenticate", "Autentica"),
|
||||
("web_id_input_tip", "È possibile inserire un ID nello stesso server, nel client web non è supportato l'accesso con IP diretto.\nSe vuoi accedere ad un dispositivo in un altro server, aggiungi l'indirizzo del server (<id>@<indirizzo_server>?key=<valore_chiave >), ad esempio,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere ad un dispositivo in un server pubblico, inserisci \"<id>@public\", la chiave non è necessaria per il server pubblico."),
|
||||
@@ -676,7 +676,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Default trackpad speed", "Velocità predefinita trackpad"),
|
||||
("Numeric one-time password", "Password numerica monouso"),
|
||||
("Enable IPv6 P2P connection", "Abilita connessione P2P IPv6"),
|
||||
("Enable UDP hole punching", "Abilita hole punching UDP"),
|
||||
("Enable UDP hole punching", "Abilita hole punching UDP"),
|
||||
("View camera", "Visualizza telecamera"),
|
||||
("Enable camera", "Abilita camera"),
|
||||
("No cameras", "Nessuna camera"),
|
||||
@@ -769,14 +769,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Reuse one connection for port forwarding", "Per l'inoltro delle porte riusa una sola connessione "),
|
||||
("port-forward-mux-tip", "Fa passare tutte le connessioni di un inoltro porte in un'unica connessione verso il dispositivo remoto, invece di connettersi e autenticarsi di nuovo per ognuna."),
|
||||
("Enable WebRTC P2P connection", "Abilita connessione P2P WebRTC"),
|
||||
("Enable TCP hole punching", "Abilita hole punching TCP"),
|
||||
("The screen sharing request was declined on the remote device", ""),
|
||||
("The screen sharing request timed out on the remote device", ""),
|
||||
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", ""),
|
||||
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", ""),
|
||||
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", ""),
|
||||
("The screen sharing request ended without completing on the remote device", ""),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", ""),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", ""),
|
||||
("Enable TCP hole punching", "Abilita hole punching TCP"),
|
||||
("The screen sharing request was declined on the remote device", "La richiesta di condivisione dello schermo nel dispositivo remoto è stata rifiutata"),
|
||||
("The screen sharing request timed out on the remote device", "La richiesta di condivisione dello schermo nel dispositivo remoto è scaduta"),
|
||||
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk non riesce a raggiungere la sessione desktop nel dispositivo remoto, controlla che sia in esecuzione una sessione desktop e che RustDesk possa usarla"),
|
||||
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Nel portale desktop nel dispositivo remoto manca una funzionalità necessaria per la condivisione dello schermo o il controllo remoto, il relativo backend potrebbe non essere installato"),
|
||||
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "La condivisione dello schermo è stata approvata nel dispositivo remoto, ma non è stato possibile aprire la connessione PipeWire"),
|
||||
("The screen sharing request ended without completing on the remote device", "La richiesta di condivisione dello schermo si è chiusa senza essere completata nel dispositivo remoto"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk non è riuscito a ottenere una schermata usabile dal portale desktop XDG, la libreria PipeWire potrebbe essere troppo vecchia"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk non è riuscito a caricare un componente GStreamer necessario per l'acquisizione dello schermo ({})"),
|
||||
("Relay fallback delay in seconds", "Ritardo fallback relay (secondi)"),
|
||||
("relay-fallback-delay-tip", "Quanto tempo una connessione relay già attiva attende la connessione WebRTC diretta prima di essere usata. Aumentalo per dare a una connessione diretta lenta più tempo per funzionare; diminuiscilo per passare prima al relay sulle reti in cui non è possibile effettuare una connessione diretta. Lascia vuoto per il valore predefinito di 2,5 secondi."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "Per avviare una chiamata vocale, attiva nella pagina 'Condivisione schermo' la voce 'Cattura audio'.")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "リモート端末での画面共有の要求は完了しないまま終了しました"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk は XDG Desktop Portal から使用可能な画面を取得できませんでした。PipeWire ライブラリが古すぎる可能性があります"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk は画面キャプチャに必要な GStreamer コンポーネントを読み込めませんでした ({})"),
|
||||
("Relay fallback delay in seconds", "中継に切り替えるまでの待ち時間 (秒)"),
|
||||
("relay-fallback-delay-tip", "すでに確立された中継接続が、直接の WebRTC 接続をどれだけ待ってから代わりに使用されるかを指定します。値を大きくすると遅い直接接続に時間を与えられ、小さくすると直接接続できないネットワークで早く中継に切り替わります。空欄にすると既定値の 2.5 秒になります。"),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -759,17 +759,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("id_whitelist_caveat_tip", "ID는 연결하는 클라이언트가 보고합니다. 화이트리스트는 노출을 줄이는 것으로 비밀번호나 2FA를 대체하지 않습니다"),
|
||||
("whitelist_cidr_tip", "CIDR 표기를 지원합니다. 예: 192.168.1.0/24"),
|
||||
("Continue", "계속"),
|
||||
("Browser didn't open? Use the url below to sign in.", "브라우저가 열리지 않았나요? 아래 URL로 로그인하세요."),
|
||||
("Browser didn't open? Use the url below to sign in.", "브라우저가 열리지 않았나요? 아래 URL을 사용하여 로그인하세요."),
|
||||
("Lock canvas", "캔버스 잠금"),
|
||||
("Sync clipboard between sessions", "세션 간 클립보드 동기화"),
|
||||
("sync-clipboard-between-sessions-tip", "하나의 원격 세션에서 복사한 텍스트나 이미지는 연결된 다른 세션의 클립보드에도 전송됩니다."),
|
||||
("terminal-clipboard-write-tip", ""),
|
||||
("Allow terminal apps to copy to clipboard", ""),
|
||||
("sync-clipboard-between-sessions-tip", "원격 세션 하나에서 복사한 텍스트나 이미지는 다른 연결된 세션의 클립보드로도 전송됩니다."),
|
||||
("terminal-clipboard-write-tip", "터미널의 앱이 이 장치의 클립보드에 텍스트를 복사하려고 합니다. 이 권한이 부여된 경우, 설정에서 이를 끌 때까지 모든 연결의 터미널 앱에 적용됩니다. 수동 복사 및 붙여넣기는 영향을 받지 않습니다."),
|
||||
("Allow terminal apps to copy to clipboard", "터미널 앱이 클립보드로 복사하도록 허용"),
|
||||
("Enable", "활성화"),
|
||||
("Reuse one connection for port forwarding", "포트 포워딩에 연결 하나를 재사용"),
|
||||
("port-forward-mux-tip", "포트 포워딩 하나의 모든 연결을 상대방과의 단일 연결로 전달합니다. 연결마다 다시 접속하고 로그인하지 않습니다."),
|
||||
("Enable WebRTC P2P connection", "WebRTC P2P 연결 사용"),
|
||||
("Enable TCP hole punching", "TCP 홀 펀칭 사용"),
|
||||
("Reuse one connection for port forwarding", "포트 포워딩을 위해 하나의 연결을 재사용"),
|
||||
("port-forward-mux-tip", "각 연결마다 다시 연결하고 로그인할 필요 없이, 포트 포워딩 매핑의 모든 연결을 피어에 대한 단일 연결로 전달합니다."),
|
||||
("Enable WebRTC P2P connection", "WebRTC P2P 연결 사용함"),
|
||||
("Enable TCP hole punching", "TCP 홀 펀칭 사용함"),
|
||||
("The screen sharing request was declined on the remote device", "원격 장치에서 화면 공유 요청이 거부되었습니다"),
|
||||
("The screen sharing request timed out on the remote device", "원격 장치에서 화면 공유 요청이 시간 초과되었습니다"),
|
||||
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk가 원격 장치의 데스크톱 세션에 접근할 수 없습니다. 세션이 실행 중이고 RustDesk가 사용할 수 있는지 확인하세요"),
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "원격 장치의 화면 공유 요청이 완료되지 않은 채 종료되었습니다"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk가 XDG Desktop Portal에서 사용 가능한 화면을 가져오지 못했습니다. PipeWire 라이브러리가 너무 오래되었을 수 있습니다"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk가 화면 캡처에 필요한 GStreamer 구성 요소를 불러오지 못했습니다 ({})"),
|
||||
("Relay fallback delay in seconds", "릴레이 대체 작동 지연 시간 (초)"),
|
||||
("relay-fallback-delay-tip", "이미 연결된 중계 연결이 직접 WebRTC 연결을 얼마나 기다린 후 대신 사용되는지입니다. 값을 늘리면 느린 직접 연결에 더 많은 시간을 주고, 줄이면 직접 연결이 불가능한 네트워크에서 더 빨리 중계로 전환합니다. 비워 두면 기본값 2.5초가 사용됩니다."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "음성 통화를 시작하려면 '화면 공유' 페이지에서 '오디오 캡처'를 사용함으로 하세요.")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Қашықтағы құрылғыдағы экранды бөлісу сұрауы аяқталмай тоқтады"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk XDG Desktop Portal-дан жарамды экран ала алмады, PipeWire кітапханасы тым ескі болуы мүмкін"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk экранды түсіру үшін қажет GStreamer компонентін жүктей алмады ({})"),
|
||||
("Relay fallback delay in seconds", "Релеге ауысу кідірісі, секундпен"),
|
||||
("relay-fallback-delay-tip", "Бұрыннан орнатылған реле байланысы тікелей WebRTC байланысын қанша уақыт күтеді, содан кейін оның орнына қолданылады. Баяу тікелей байланысқа көбірек уақыт беру үшін үлкейтіңіз; тікелей байланыс мүмкін емес желілерде релеге тезірек ауысу үшін кішірейтіңіз. Әдепкі 2.5 секунд үшін бос қалдырыңыз."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Ekrano bendrinimo užklausa nuotoliniame įrenginyje baigėsi jos neužbaigus"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk negavo tinkamo ekrano iš XDG Desktop Portal, PipeWire biblioteka gali būti per sena"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nepavyko įkelti ekrano įrašymui reikalingo GStreamer komponento ({})"),
|
||||
("Relay fallback delay in seconds", "Delsa prieš pereinant prie perdavimo sekundėmis"),
|
||||
("relay-fallback-delay-tip", "Kiek laiko jau užmegztas perdavimo ryšys laukia tiesioginio WebRTC ryšio, kol bus panaudotas vietoj jo. Padidinkite, kad lėtam tiesioginiam ryšiui būtų skirta daugiau laiko; sumažinkite, kad tinkluose, kuriuose tiesioginis ryšys neįmanomas, greičiau būtų pereinama prie perdavimo. Palikite tuščią numatytajai 2.5 sekundės reikšmei."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Ekrāna koplietošanas pieprasījums attālinātajā ierīcē beidzās, netiekot pabeigts"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk neieguva izmantojamu ekrānu no XDG Desktop Portal, PipeWire bibliotēka var būt pārāk veca"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nevarēja ielādēt ekrāna tveršanai nepieciešamo GStreamer komponentu ({})"),
|
||||
("Relay fallback delay in seconds", "Aizkave pirms pārslēgšanās uz retranslatoru sekundēs"),
|
||||
("relay-fallback-delay-tip", "Cik ilgi jau izveidots retranslatora savienojums gaida tiešo WebRTC savienojumu, pirms tiek izmantots tā vietā. Palieliniet, lai lēnam tiešajam savienojumam dotu vairāk laika; samaziniet, lai tīklos, kur tiešais savienojums nav iespējams, ātrāk pārslēgtos uz retranslatoru. Atstājiet tukšu noklusējuma 2.5 sekunžu vērtībai."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "വിദൂര ഉപകരണത്തിലെ സ്ക്രീൻ പങ്കിടൽ അഭ്യർത്ഥന പൂർത്തിയാകാതെ അവസാനിച്ചു"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "XDG Desktop Portal-ൽ നിന്ന് ഉപയോഗയോഗ്യമായ സ്ക്രീൻ RustDesk-ന് ലഭിച്ചില്ല, PipeWire ലൈബ്രറി വളരെ പഴയതാകാം"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "സ്ക്രീൻ പകർത്താൻ ആവശ്യമായ GStreamer ഘടകം RustDesk-ന് ലോഡ് ചെയ്യാനായില്ല ({})"),
|
||||
("Relay fallback delay in seconds", "റിലേയിലേക്ക് മാറുന്നതിനുള്ള കാലതാമസം സെക്കൻഡിൽ"),
|
||||
("relay-fallback-delay-tip", "ഇതിനകം സ്ഥാപിതമായ റിലേ കണക്ഷൻ നേരിട്ടുള്ള WebRTC കണക്ഷനായി എത്ര നേരം കാത്തിരിക്കുന്നു, അതിനുശേഷം അതിനുപകരം ഉപയോഗിക്കുന്നു. മന്ദഗതിയിലുള്ള നേരിട്ടുള്ള കണക്ഷന് കൂടുതൽ സമയം നൽകാൻ വർദ്ധിപ്പിക്കുക; നേരിട്ടുള്ള കണക്ഷൻ സാധ്യമല്ലാത്ത നെറ്റ്വർക്കുകളിൽ വേഗത്തിൽ റിലേയിലേക്ക് മാറാൻ കുറയ്ക്കുക. സ്ഥിരസ്ഥിതിയായ 2.5 സെക്കൻഡിനായി ശൂന്യമാക്കിയിടുക."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Forespørselen om skjermdeling på den eksterne enheten ble avsluttet uten å bli fullført"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk fikk ingen brukbar skjerm fra XDG Desktop Portal, PipeWire-biblioteket kan være for gammelt"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kunne ikke laste en GStreamer-komponent som kreves for skjermopptak ({})"),
|
||||
("Relay fallback delay in seconds", "Forsinkelse før bruk av relé i sekunder"),
|
||||
("relay-fallback-delay-tip", "Hvor lenge en allerede opprettet reléforbindelse venter på den direkte WebRTC-forbindelsen før den brukes i stedet. Øk verdien for å gi en treg direkteforbindelse mer tid; senk den for å gå raskere over til reléet på nettverk der direkte forbindelse ikke er mulig. La stå tom for standardverdien på 2.5 sekunder."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Het verzoek om schermdeling op het externe apparaat is geëindigd zonder te zijn voltooid"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk kon geen bruikbaar scherm verkrijgen van de XDG Desktop Portal, de PipeWire-bibliotheek is mogelijk te oud"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kon een GStreamer-component die nodig is voor schermopname niet laden ({})"),
|
||||
("Relay fallback delay in seconds", "Vertraging voordat relay wordt gebruikt in seconden"),
|
||||
("relay-fallback-delay-tip", "Hoe lang een al tot stand gekomen relayverbinding wacht op de directe WebRTC-verbinding voordat deze in plaats daarvan wordt gebruikt. Verhoog de waarde om een trage directe verbinding meer tijd te geven; verlaag deze om op netwerken waar een directe verbinding niet mogelijk is sneller op de relay terug te vallen. Laat leeg voor de standaardwaarde van 2.5 seconden."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Żądanie udostępnienia ekranu na urządzeniu zdalnym zakończyło się bez ukończenia"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nie uzyskał użytecznego ekranu z XDG Desktop Portal, biblioteka PipeWire może być zbyt stara"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nie mógł załadować składnika GStreamer wymaganego do przechwytywania ekranu ({})"),
|
||||
("Relay fallback delay in seconds", "Opóźnienie przed przejściem na przekaźnik w sekundach"),
|
||||
("relay-fallback-delay-tip", "Jak długo nawiązane już połączenie przez przekaźnik czeka na bezpośrednie połączenie WebRTC, zanim zostanie użyte zamiast niego. Zwiększ, aby dać wolnemu połączeniu bezpośredniemu więcej czasu; zmniejsz, aby w sieciach, w których połączenie bezpośrednie jest niemożliwe, szybciej przechodzić na przekaźnik. Pozostaw puste, aby użyć wartości domyślnej 2.5 sekundy."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "O pedido de partilha de ecrã no dispositivo remoto terminou sem ser concluído"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "O RustDesk não conseguiu obter um ecrã utilizável do XDG Desktop Portal, a biblioteca PipeWire pode ser demasiado antiga"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "O RustDesk não conseguiu carregar um componente do GStreamer necessário para capturar o ecrã ({})"),
|
||||
("Relay fallback delay in seconds", "Atraso antes de recorrer ao retransmissor em segundos"),
|
||||
("relay-fallback-delay-tip", "Quanto tempo uma ligação de retransmissão já estabelecida aguarda pela ligação direta WebRTC antes de ser usada em vez dela. Aumente para dar mais tempo a uma ligação direta lenta; diminua para recorrer mais cedo ao retransmissor em redes onde não é possível uma ligação direta. Deixe vazio para o valor predefinido de 2.5 segundos."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Click to upgrade", "Iniciar atualização"),
|
||||
("Configure", "Configurar"),
|
||||
("config_acc", "Para controlar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Acessibilidade\"."),
|
||||
("config_screen", "Para acessar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\"/"),
|
||||
("config_screen", "Para acessar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\""),
|
||||
("Installing ...", "Instalando ..."),
|
||||
("Install", "Instalar"),
|
||||
("Installation", "Instalação"),
|
||||
@@ -282,7 +282,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("android_service_will_start_tip", "Habilitar a Captura de Tela irá automaticamente inicalizar o serviço, permitindo que outros dispositivos solicitem uma conexão deste dispositivo."),
|
||||
("android_stop_service_tip", "Fechar o serviço irá automaticamente fechar todas as conexões estabelecidas."),
|
||||
("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor atualize para o Android 10 ou superior."),
|
||||
("android_start_service_tip", "Toque em [Iniciar serviço] ou habilite a permissão [Captura de tela] para iniciar o serviço de compartilhamento de tela."),
|
||||
("android_start_service_tip", "Toque em [Iniciar Serviço] ou habilite a permissão [Captura de Tela] para iniciar o serviço de compartilhamento de tela."),
|
||||
("android_permission_may_not_change_tip", "As permissões para conexões estabelecidas podem não ser alteradas instantaneamente até que seja reconectado."),
|
||||
("Account", "Conta"),
|
||||
("Overwrite", "Substituir"),
|
||||
@@ -691,7 +691,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Incorrect username or password.", "Usuário ou senha incorretos"),
|
||||
("The user is not an administrator.", "O usuário não é administrador"),
|
||||
("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"),
|
||||
("Supported only in the installed version.", "Funciona somente na versão instalada"),
|
||||
("Supported only in the installed version.", "Suportado somente na versão instalada"),
|
||||
("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"),
|
||||
("Preparing for installation ...", "Preparando para instalação ..."),
|
||||
("Show my cursor", "Mostrar meu cursor"),
|
||||
@@ -763,20 +763,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Lock canvas", "Bloquear tela"),
|
||||
("Sync clipboard between sessions", "Sincronizar área de transferência entre sessões"),
|
||||
("sync-clipboard-between-sessions-tip", "Texto ou imagens copiados em uma sessão remota também são enviados para a área de transferência das suas outras sessões conectadas."),
|
||||
("terminal-clipboard-write-tip", ""),
|
||||
("Allow terminal apps to copy to clipboard", ""),
|
||||
("terminal-clipboard-write-tip", "Aplicativos do terminal podem copiar para a área de transferência"),
|
||||
("Allow terminal apps to copy to clipboard", "Permitir cópia do terminal para a área de transferência"),
|
||||
("Enable", "Habilitar"),
|
||||
("Reuse one connection for port forwarding", "Reutilizar uma conexão para encaminhamento de portas"),
|
||||
("port-forward-mux-tip", "Levar todas as conexões de um encaminhamento de portas por uma única conexão com o outro computador, em vez de conectar e fazer login novamente para cada uma."),
|
||||
("port-forward-mux-tip", "Levar todas as conexões de um encaminhamento de portas por uma única conexão com o outro computador, em vez de estabelecer uma nova conexão e fazer login novamente para cada uma."),
|
||||
("Enable WebRTC P2P connection", "Habilitar conexão WebRTC P2P"),
|
||||
("Enable TCP hole punching", "Habilitar TCP hole punching"),
|
||||
("The screen sharing request was declined on the remote device", "A solicitação de compartilhamento de tela foi recusada no dispositivo remoto"),
|
||||
("The screen sharing request timed out on the remote device", "A solicitação de compartilhamento de tela expirou no dispositivo remoto"),
|
||||
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "O RustDesk não consegue acessar a sessão de área de trabalho no dispositivo remoto, verifique se há uma sessão em execução e se o RustDesk pode usá-la"),
|
||||
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Falta ao portal de área de trabalho do dispositivo remoto um recurso necessário para compartilhamento de tela ou controle remoto, seu backend pode não estar instalado"),
|
||||
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "O compartilhamento de tela foi aprovado no dispositivo remoto, mas não foi possível abrir a conexão PipeWire"),
|
||||
("The screen sharing request ended without completing on the remote device", "A solicitação de compartilhamento de tela no dispositivo remoto terminou sem ser concluída"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "O RustDesk não conseguiu obter uma tela utilizável do XDG Desktop Portal, a biblioteca PipeWire pode ser muito antiga"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "O RustDesk não conseguiu carregar um componente do GStreamer necessário para capturar a tela ({})"),
|
||||
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "O RustDesk não consegue acessar a sessão da área de trabalho no dispositivo remoto, verifique se há uma sessão da área de trabalho em execução e se o RustDesk pode acessá-la."),
|
||||
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "O portal da área de trabalho no dispositivo remoto não possui um recurso necessário para o compartilhamento de tela ou controle remoto. O backend pode não estar instalado."),
|
||||
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "O compartilhamento de tela foi autorizado no dispositivo remoto, mas não foi possível abrir a conexão com o PipeWire."),
|
||||
("The screen sharing request ended without completing on the remote device", "A solicitação de compartilhamento de tela no dispositivo remoto foi encerrada sem ser concluída."),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "O RustDesk não conseguiu obter uma tela utilizável do XDG Desktop Portal. A biblioteca do PipeWire pode estar desatualizada."),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "O RustDesk não conseguiu carregar um componente do GStreamer necessário para a captura de tela ({})."),
|
||||
("Relay fallback delay in seconds", "Atraso antes de recorrer ao retransmissor em segundos"),
|
||||
("relay-fallback-delay-tip", "Tempo que a conexão de retransmissão aguarda pela conexão direta WebRTC. Aumente para dar mais tempo a conexões lentas; diminua para usar o retransmissor mais cedo. Deixe vazio para usar o padrão de 2,5 segundos."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "Para iniciar uma chamada de voz, ative \"Captura de áudio\" na página \"Compartilhamento de tela\".")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Cererea de partajare a ecranului pe dispozitivul de la distanță s-a încheiat fără a fi finalizată"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nu a putut obține un ecran utilizabil de la XDG Desktop Portal, biblioteca PipeWire poate fi prea veche"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nu a putut încărca o componentă GStreamer necesară pentru capturarea ecranului ({})"),
|
||||
("Relay fallback delay in seconds", "Întârziere înainte de trecerea la releu în secunde"),
|
||||
("relay-fallback-delay-tip", "Cât timp așteaptă o conexiune prin releu deja stabilită conexiunea directă WebRTC înainte de a fi folosită în locul ei. Măriți valoarea pentru a acorda mai mult timp unei conexiuni directe lente; micșorați-o pentru a trece mai repede la releu în rețelele în care o conexiune directă nu este posibilă. Lăsați gol pentru valoarea implicită de 2.5 secunde."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -730,7 +730,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("preset-password-in-use-tip", "Установленный пароль сейчас используется."),
|
||||
("Enable privacy mode", "Использовать режим конфиденциальности"),
|
||||
("allow-remote-toolbar-docking-any-edge", "Разрешать прикрепление удалённой панели инструментов к любому краю окна"),
|
||||
("API Token", "API-токен"),
|
||||
("API Token", "Токен API"),
|
||||
("Deploy", "Развернуть"),
|
||||
("Custom ID (optional)", "Пользовательский ID (необязательно)"),
|
||||
("server_requires_deployment_tip", "Сервер требует явного развёртывания этого устройства. Развернуть сейчас?"),
|
||||
@@ -748,23 +748,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show on the minimized toolbar", "Показывать на свёрнутой панели инструментов"),
|
||||
("All monitors", "Все мониторы"),
|
||||
("#{} monitor", "Монитор {}"),
|
||||
("conn-e2ee-unavailable-tip", "Не удалось проверить сквозное шифрование.\nУдаленное устройство, возможно, еще настраивается. Повторите попытку позже.\nЕсли это повторяется, сервер может быть ненадежным.\nВсе равно продолжить?"),
|
||||
("conn-e2ee-unavailable-tip", "Невозможно проверить сквозное шифрование.\nУдалённое устройство, возможно, ещё настраивается. Повторите попытку позже.\nЕсли это повторяется, сервер может быть ненадёжным.\nВсё равно продолжить?"),
|
||||
("ID whitelisting", "Список разрешённых ID"),
|
||||
("Use ID whitelisting", "Использовать белый список ID"),
|
||||
("id_whitelist_tip", "Только ID из белого списка могут получить доступ к моему устройству."),
|
||||
("id_whitelist_wildcard_tip", "Поддерживаются подстановочные знаки: '*' соответствует любому количеству символов, '?' — ровно одному символу"),
|
||||
("id_whitelist_wildcard_tip", "Поддерживаются подстановочные знаки: \"*\" соответствует любому количеству символов, \"?\" — ровно одному символу"),
|
||||
("Invalid ID", "Неправильный ID"),
|
||||
("Your ID is blocked by the peer", "Ваш ID заблокирован удалённым устройством"),
|
||||
("Your ip is blocked by the peer", "Ваш IP-адрес заблокирован удалённым устройством"),
|
||||
("id_whitelist_caveat_tip", "ID сообщается подключающимся клиентом. Белый список уменьшает поверхность атаки и не заменяет пароль или 2FA"),
|
||||
("whitelist_cidr_tip", "Поддерживается нотация CIDR, например 192.168.1.0/24"),
|
||||
("whitelist_cidr_tip", "Поддерживается нотация CIDR, например: 192.168.1.0/24"),
|
||||
("Continue", "Продолжить"),
|
||||
("Browser didn't open? Use the url below to sign in.", "Браузер не открылся? Используйте ссылку ниже для входа."),
|
||||
("Lock canvas", "Заблокировать холст"),
|
||||
("Sync clipboard between sessions", "Синхронизировать буфер обмена между сеансами"),
|
||||
("sync-clipboard-between-sessions-tip", "Текст или изображения, скопированные в одном удалённом сеансе, также отправляются в буфер обмена других подключённых сеансов."),
|
||||
("terminal-clipboard-write-tip", ""),
|
||||
("Allow terminal apps to copy to clipboard", ""),
|
||||
("terminal-clipboard-write-tip", "Приложение в терминале хочет скопировать текст в буфер обмена этого устройства. Если соответствующее разрешение предоставлено, оно применяется к приложениям в терминале во всех соединениях, пока вы не отключите его в настройках. Ручные копирование и вставка не затрагиваются."),
|
||||
("Allow terminal apps to copy to clipboard", "Разрешить приложениям в терминале копирование в буфер обмена"),
|
||||
("Enable", "Включить"),
|
||||
("Reuse one connection for port forwarding", "Использовать одно подключение для перенаправления портов"),
|
||||
("port-forward-mux-tip", "Передавать все соединения одного перенаправления портов через одно подключение к удалённому устройству вместо повторного подключения и входа для каждого из них."),
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Запрос на демонстрацию экрана на удалённом устройстве завершился, не будучи выполненным"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk не смог получить пригодный экран от XDG Desktop Portal, библиотека PipeWire может быть слишком старой"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не удалось загрузить компонент GStreamer, необходимый для захвата экрана ({})"),
|
||||
("Relay fallback delay in seconds", "Задержка перед переходом на ретранслятор в секундах"),
|
||||
("relay-fallback-delay-tip", "Сколько времени уже установленное соединение через ретранслятор ждёт прямое соединение WebRTC, прежде чем будет использовано вместо него. Увеличьте, чтобы дать медленному прямому соединению больше времени; уменьшите, чтобы быстрее переходить на ретранслятор в сетях, где прямое соединение невозможно. Оставьте пустым для значения по умолчанию 2.5 секунды."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "Чтобы начать голосовой вызов, включите \"Захват аудио\" на странице \"Демонстрация экрана\" настроек.")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Sa rechesta de cumpartzidura de sa schermada in su dispositivu remotu est acabada chene si cumpletare"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk no at pòdidu otènnere una schermada impreabile dae XDG Desktop Portal, sa libreria PipeWire podet èssere tropu betza"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk no at pòdidu carrigare unu cumponente de GStreamer netzessàriu pro registrare sa schermada ({})"),
|
||||
("Relay fallback delay in seconds", "Tardu prima de impreare su relè in segundos"),
|
||||
("relay-fallback-delay-tip", "Cantu tempus una connessione de relè giai istabilida abetat sa connessione direta WebRTC prima de èssere impreada in su postu suo. Aumenta pro dare prus tempus a una connessione direta lenta; diminuì pro colare prima a su relè in sas retes in ue non si podet fàghere una connessione direta. Lassa bòidu pro su valore predefinidu de 2.5 segundos."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Žiadosť o zdieľanie obrazovky na vzdialenom zariadení sa skončila bez dokončenia"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nezískal z XDG Desktop Portal použiteľnú obrazovku, knižnica PipeWire môže byť príliš stará"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nedokázal načítať komponent GStreamera potrebný na zachytenie obrazovky ({})"),
|
||||
("Relay fallback delay in seconds", "Oneskorenie pred prepnutím na prenos v sekundách"),
|
||||
("relay-fallback-delay-tip", "Ako dlho už nadviazané spojenie cez prenos čaká na priame spojenie WebRTC, kým sa použije namiesto neho. Zvýšte, aby pomalé priame spojenie malo viac času; znížte, aby sa v sieťach, kde priame spojenie nie je možné, skôr prešlo na prenos. Nechajte prázdne pre predvolenú hodnotu 2.5 sekundy."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Zahteva za skupno rabo zaslona na oddaljeni napravi se je končala, ne da bi bila dokončana"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk od XDG Desktop Portala ni dobil uporabnega zaslona, knjižnica PipeWire je morda prestara"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ni mogel naložiti komponente GStreamer, potrebne za zajem zaslona ({})"),
|
||||
("Relay fallback delay in seconds", "Zakasnitev pred preklopom na posrednika v sekundah"),
|
||||
("relay-fallback-delay-tip", "Kako dolgo že vzpostavljena posredniška povezava čaka na neposredno povezavo WebRTC, preden se uporabi namesto nje. Povečajte, da počasni neposredni povezavi date več časa; zmanjšajte, da v omrežjih, kjer neposredna povezava ni mogoča, hitreje preklopite na posrednika. Pustite prazno za privzeto vrednost 2.5 sekunde."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("android_service_will_start_tip", "Aktivizimi i \"Regjistrimi i ekranit\" do të nisë automatikisht shërbimin, duke lejuar pajisjet e tjera të kërkojnë një lidhje me pajisjen tuaj."),
|
||||
("android_stop_service_tip", "Mbyllja e shërbimit do të mbyllë automatikisht të gjitha lidhjet e vendosura."),
|
||||
("android_version_audio_tip", "Versioni aktual i Android nuk mbështet regjistrimin e audios, ju lutemi përmirësoni në Android 10 ose më të lartë."),
|
||||
("android_start_service_tip", "Trokitni te [Nis shërbimin] ose aktivizoni lejen [Kapja e ekranit] për të nisur shërbimin e ndarjes së ekranit."),
|
||||
("android_start_service_tip", "Trokitni te [Nis Shërbimin] ose aktivizoni lejen [Kapja e ekranit] për të nisur shërbimin e ndarjes së ekranit."),
|
||||
("android_permission_may_not_change_tip", "Lejet për lidhjet e themeluara mund të mos ndryshohen menjëherë derisa të rilidheni."),
|
||||
("Account", "Llogaria"),
|
||||
("Overwrite", "Përshkruaj"),
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Kërkesa për ndarjen e ekranit në pajisjen e largët përfundoi pa u kryer"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nuk mori një ekran të përdorshëm nga XDG Desktop Portal, biblioteka PipeWire mund të jetë shumë e vjetër"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nuk mundi të ngarkojë një komponent të GStreamer të nevojshëm për regjistrimin e ekranit ({})"),
|
||||
("Relay fallback delay in seconds", "Vonesa para kalimit te releja në sekonda"),
|
||||
("relay-fallback-delay-tip", "Sa gjatë pret një lidhje releje tashmë e vendosur lidhjen e drejtpërdrejtë WebRTC përpara se të përdoret në vend të saj. Rriteni për t'i dhënë më shumë kohë një lidhjeje të drejtpërdrejtë të ngadaltë; uleni për të kaluar më shpejt te releja në rrjete ku lidhja e drejtpërdrejtë nuk është e mundur. Lëreni bosh për vlerën e parazgjedhur prej 2.5 sekondash."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Zahtev za deljenje ekrana na udaljenom uređaju završio se bez dovršetka"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nije mogao da dobije upotrebljiv ekran od XDG Desktop Portala, PipeWire biblioteka je možda prestara"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nije mogao da učita GStreamer komponentu potrebnu za snimanje ekrana ({})"),
|
||||
("Relay fallback delay in seconds", "Кашњење пре преласка на релеј у секундама"),
|
||||
("relay-fallback-delay-tip", "Колико дуго већ успостављена релејна веза чека на директну WebRTC везу пре него што се употреби уместо ње. Повећајте да бисте спорој директној вези дали више времена; смањите да бисте на мрежама где директна веза није могућа брже прешли на релеј. Оставите празно за подразумевану вредност од 2.5 секунде."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("android_input_permission_tip1", "Android rättigheter saknas"),
|
||||
("android_input_permission_tip2", "Gå till systeminställningarna, hitta [Installed Services], sätt på [RustDesk Input] tjänsten."),
|
||||
("android_new_connection_tip", "Ny kontrollförfrågan mottagen, denna vill kontrollera din enhet."),
|
||||
("android_service_will_start_tip", "Sätter du på \"skärminspelning\" kommer tjänsten automatiskt att starta. Detta tillåter andra enheter att kontrollera din enhet."),
|
||||
("android_service_will_start_tip", "Sätter du på \"Skärminspelning\" kommer tjänsten automatiskt att starta. Detta tillåter andra enheter att kontrollera din enhet."),
|
||||
("android_stop_service_tip", "Genom att stänga av tjänsten kommer alla enheter att kopplas ifrån."),
|
||||
("android_version_audio_tip", "Din version av Android stödjer inte ljudinspelning, Android 10 eller nyare krävs"),
|
||||
("android_start_service_tip", "android_start_service_tips"),
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Begäran om skärmdelning på fjärrenheten avslutades utan att slutföras"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk fick ingen användbar skärm från XDG Desktop Portal, PipeWire-biblioteket kan vara för gammalt"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kunde inte läsa in en GStreamer-komponent som krävs för skärminspelning ({})"),
|
||||
("Relay fallback delay in seconds", "Fördröjning innan relä används i sekunder"),
|
||||
("relay-fallback-delay-tip", "Hur länge en redan upprättad reläanslutning väntar på den direkta WebRTC-anslutningen innan den används i stället. Öka värdet för att ge en långsam direktanslutning mer tid; sänk det för att snabbare falla tillbaka på reläet i nätverk där direktanslutning inte är möjlig. Lämna tomt för standardvärdet 2.5 sekunder."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "தொலைநிலை சாதனத்தில் திரை பகிர்வு கோரிக்கை நிறைவடையாமல் முடிந்தது"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "XDG Desktop Portal-லிருந்து பயன்படுத்தக்கூடிய திரையை RustDesk பெற முடியவில்லை, PipeWire நூலகம் மிகவும் பழையதாக இருக்கலாம்"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "திரைப் பதிவுக்குத் தேவையான GStreamer கூறை RustDesk ஏற்ற முடியவில்லை ({})"),
|
||||
("Relay fallback delay in seconds", "ரிலேக்கு மாறுவதற்கான தாமதம் வினாடிகளில்"),
|
||||
("relay-fallback-delay-tip", "ஏற்கனவே நிறுவப்பட்ட ரிலே இணைப்பு நேரடி WebRTC இணைப்புக்காக எவ்வளவு நேரம் காத்திருக்கிறது, அதன் பிறகு அதற்குப் பதிலாகப் பயன்படுத்தப்படுகிறது. மெதுவான நேரடி இணைப்புக்கு அதிக நேரம் வழங்க அதிகரிக்கவும்; நேரடி இணைப்பு சாத்தியமில்லாத பிணையங்களில் விரைவாக ரிலேக்கு மாற குறைக்கவும். இயல்புநிலை 2.5 வினாடிகளுக்கு காலியாக விடவும்."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", ""),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", ""),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", ""),
|
||||
("Relay fallback delay in seconds", ""),
|
||||
("relay-fallback-delay-tip", ""),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "คำขอแชร์หน้าจอบนอุปกรณ์ระยะไกลสิ้นสุดลงโดยไม่เสร็จสมบูรณ์"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk ไม่สามารถรับหน้าจอที่ใช้งานได้จาก XDG Desktop Portal ไลบรารี PipeWire อาจเก่าเกินไป"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ไม่สามารถโหลดส่วนประกอบ GStreamer ที่จำเป็นสำหรับการบันทึกหน้าจอได้ ({})"),
|
||||
("Relay fallback delay in seconds", "เวลารอก่อนเปลี่ยนไปใช้รีเลย์ (วินาที)"),
|
||||
("relay-fallback-delay-tip", "การเชื่อมต่อผ่านรีเลย์ที่สร้างไว้แล้วจะรอการเชื่อมต่อ WebRTC โดยตรงนานเท่าใดก่อนที่จะถูกใช้แทน เพิ่มค่าเพื่อให้การเชื่อมต่อโดยตรงที่ช้ามีเวลามากขึ้น ลดค่าเพื่อเปลี่ยนไปใช้รีเลย์เร็วขึ้นในเครือข่ายที่ไม่สามารถเชื่อมต่อโดยตรงได้ เว้นว่างไว้เพื่อใช้ค่าเริ่มต้น 2.5 วินาที"),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -441,7 +441,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Voice call", "Sesli görüşme"),
|
||||
("Text chat", "Metin sohbeti"),
|
||||
("Stop voice call", "Sesli görüşmeyi durdur"),
|
||||
("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; aktarmalı bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde aktarma sunucusu kullanmak istiyorsanız ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her Zaman Aktarmalı Üzerinden Bağlan\" seçeneğini seçebilirsiniz."),
|
||||
("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; aktarmalı bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde aktarma sunucusu kullanmak istiyorsanız ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her zaman aktarmalı üzerinden bağlan\" seçeneğini seçebilirsiniz."),
|
||||
("Reconnect", "Yeniden Bağlan"),
|
||||
("Codec", "Kodlayıcı"),
|
||||
("Resolution", "Çözünürlük"),
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Uzak cihazdaki ekran paylaşımı isteği tamamlanmadan sona erdi"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk, XDG Desktop Portal'dan kullanılabilir bir ekran alamadı, PipeWire kitaplığı çok eski olabilir"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ekran yakalama için gereken GStreamer bileşenini yükleyemedi ({})"),
|
||||
("Relay fallback delay in seconds", "Aktarıcıya geçiş gecikmesi (saniye)"),
|
||||
("relay-fallback-delay-tip", "Zaten kurulmuş bir aktarıcı bağlantısının, onun yerine kullanılmadan önce doğrudan WebRTC bağlantısını ne kadar beklediğidir. Yavaş bir doğrudan bağlantıya daha fazla süre tanımak için artırın; doğrudan bağlantının kurulamadığı ağlarda aktarıcıya daha erken geçmek için azaltın. Varsayılan 2.5 saniye için boş bırakın."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "遠端裝置上的螢幕分享要求已結束,但未完成"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk 無法從 XDG Desktop Portal 取得可用的螢幕,PipeWire 函式庫可能過舊"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk 無法載入螢幕擷取所需的 GStreamer 元件 ({})"),
|
||||
("Relay fallback delay in seconds", "回退到中繼前的等待時間(秒)"),
|
||||
("relay-fallback-delay-tip", "已經建立的中繼連線會等待直連的 WebRTC 多久,超過這個時間就改用中繼。調大可以讓較慢的直連有更多機會勝出;調小則在無法直連的網路上更快回退到中繼。留空表示使用預設值 2.5 秒。"),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Запит на демонстрацію екрана на віддаленому пристрої завершився, не будучи виконаним"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk не зміг отримати придатний екран від XDG Desktop Portal, бібліотека PipeWire може бути застарою"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не вдалося завантажити компонент GStreamer, потрібний для захоплення екрана ({})"),
|
||||
("Relay fallback delay in seconds", "Затримка перед переходом на ретранслятор у секундах"),
|
||||
("relay-fallback-delay-tip", "Скільки часу вже встановлене з'єднання через ретранслятор чекає на пряме з'єднання WebRTC, перш ніж буде використане замість нього. Збільште, щоб дати повільному прямому з'єднанню більше часу; зменште, щоб швидше переходити на ретранслятор у мережах, де пряме з'єднання неможливе. Залиште порожнім для типового значення 2.5 секунди."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -778,6 +778,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "ریموٹ ڈیوائس پر اسکرین شیئرنگ کی درخواست مکمل ہوئے بغیر ختم ہو گئی"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk کو XDG Desktop Portal سے قابلِ استعمال اسکرین نہیں مل سکی، PipeWire لائبریری شاید بہت پرانی ہے"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk اسکرین ریکارڈنگ کے لیے درکار GStreamer جزو لوڈ نہیں کر سکا ({})"),
|
||||
("Relay fallback delay in seconds", "ریلے پر واپس جانے میں تاخیر سیکنڈ میں"),
|
||||
("relay-fallback-delay-tip", "پہلے سے قائم ریلے کنکشن براہ راست WebRTC کنکشن کا کتنی دیر انتظار کرتا ہے، اس کے بعد اس کی جگہ استعمال ہوتا ہے۔ سست براہ راست کنکشن کو مزید وقت دینے کے لیے بڑھائیں؛ ان نیٹ ورکس پر جہاں براہ راست کنکشن ممکن نہیں، جلد ریلے پر جانے کے لیے کم کریں۔ پہلے سے طے شدہ 2.5 سیکنڈ کے لیے خالی چھوڑ دیں۔"),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
|
||||
@@ -778,5 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("The screen sharing request ended without completing on the remote device", "Yêu cầu chia sẻ màn hình trên thiết bị từ xa đã kết thúc mà chưa hoàn tất"),
|
||||
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk không lấy được màn hình dùng được từ XDG Desktop Portal, thư viện PipeWire có thể quá cũ"),
|
||||
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk không thể tải một thành phần GStreamer cần cho việc ghi màn hình ({})"),
|
||||
("Relay fallback delay in seconds", "Độ trễ trước khi chuyển sang trung chuyển (giây)"),
|
||||
("relay-fallback-delay-tip", "Kết nối trung chuyển đã thiết lập sẽ chờ kết nối WebRTC trực tiếp trong bao lâu trước khi được dùng thay thế. Tăng giá trị để cho kết nối trực tiếp chậm thêm thời gian; giảm để chuyển sang trung chuyển sớm hơn trên các mạng không thể kết nối trực tiếp. Để trống để dùng giá trị mặc định 2.5 giây."),
|
||||
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(any(test, not(target_os = "linux")))]
|
||||
mod audio_resampler;
|
||||
mod keyboard;
|
||||
/// cbindgen:ignore
|
||||
pub mod platform;
|
||||
|
||||
@@ -542,6 +542,26 @@ extern "C"
|
||||
SHAddToRecentDocs(SHARD_PATHW, path);
|
||||
}
|
||||
|
||||
// Hyper-V Enhanced Session names do not have the usual "rdp" prefix.
|
||||
static bool is_rdp_session_by_protocol(DWORD session_id)
|
||||
{
|
||||
LPSTR buffer = nullptr;
|
||||
DWORD bytes = 0;
|
||||
if (!WTSQuerySessionInformationA(
|
||||
WTS_CURRENT_SERVER_HANDLE, session_id, WTSClientProtocolType, &buffer, &bytes)) {
|
||||
flog("Failed to query protocol for session %lu: Windows error %lu\n",
|
||||
session_id, GetLastError());
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<char, decltype(&WTSFreeMemory)> protocol_info(buffer, WTSFreeMemory);
|
||||
if (!buffer || bytes < sizeof(USHORT)) {
|
||||
flog("Failed to query protocol for session %lu: Windows error %lu\n",
|
||||
session_id, static_cast<DWORD>(ERROR_INVALID_DATA));
|
||||
return false;
|
||||
}
|
||||
return *reinterpret_cast<const USHORT *>(buffer) == WTS_PROTOCOL_TYPE_RDP;
|
||||
}
|
||||
|
||||
DWORD get_current_session(BOOL include_rdp)
|
||||
{
|
||||
auto rdp_or_console = WTSGetActiveConsoleSessionId();
|
||||
@@ -573,6 +593,10 @@ extern "C"
|
||||
{
|
||||
rdp_or_console = info.SessionId;
|
||||
}
|
||||
else if (is_rdp_session_by_protocol(info.SessionId))
|
||||
{
|
||||
rdp_or_console = info.SessionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
WTSFreeMemory(pInfos);
|
||||
@@ -666,6 +690,9 @@ extern "C"
|
||||
else if (include_rdp && !strnicmp(info.pWinStationName, ica, nica)) {
|
||||
sessionIds.push_back(std::wstring(L"ICA:") + std::to_wstring(info.SessionId));
|
||||
}
|
||||
else if (include_rdp && is_rdp_session_by_protocol(info.SessionId)) {
|
||||
sessionIds.push_back(std::wstring(L"RDP:") + std::to_wstring(info.SessionId));
|
||||
}
|
||||
}
|
||||
}
|
||||
WTSFreeMemory(pInfos);
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{
|
||||
hash::BuildHasher,
|
||||
net::SocketAddr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc, RwLock,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
@@ -63,6 +63,36 @@ const MAX_PENDING_REMOTE_ICE: usize = 64;
|
||||
/// Queued candidates remembered so the controller's re-send is skipped instead of taking a slot
|
||||
/// of its own. Far more than an honest peer gathers, at eight bytes each.
|
||||
const ICE_DEDUP_WINDOW: usize = 256;
|
||||
/// Answerers between an offer and an open data channel. An offer arrives before any password or
|
||||
/// accept prompt, and each one builds a peer connection that binds a socket per interface and
|
||||
/// runs ICE for up to `CONNECT_TIMEOUT`, where a forged TCP punch costs one connect. Past this
|
||||
/// many the offer is declined, and the controller carries on over punch and relay as it does
|
||||
/// for a peer without WebRTC. A guard against pathological setup concurrency, above what
|
||||
/// legitimate controllers reach at once in the seconds ICE takes; once the channel is open the
|
||||
/// connection is one like any other, and the connection layer bounds unauthenticated
|
||||
/// connections in number and in time for every transport alike.
|
||||
const MAX_WEBRTC_ANSWERERS: usize = 16;
|
||||
static WEBRTC_ANSWERERS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// One of the `MAX_WEBRTC_ANSWERERS` slots, given back on drop.
|
||||
struct AnswererSlot;
|
||||
|
||||
impl AnswererSlot {
|
||||
fn take() -> Option<Self> {
|
||||
WEBRTC_ANSWERERS
|
||||
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |n| {
|
||||
(n < MAX_WEBRTC_ANSWERERS).then(|| n + 1)
|
||||
})
|
||||
.ok()
|
||||
.map(|_| Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AnswererSlot {
|
||||
fn drop(&mut self) {
|
||||
WEBRTC_ANSWERERS.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
// The rendezvous ICE route is reachable without a prior punch and the peer decides how many
|
||||
// candidates it sends, so these sites would let someone else set how much this machine writes to
|
||||
// its log file. One line a minute each, carrying the suppressed count.
|
||||
@@ -749,6 +779,15 @@ impl RendezvousMediator {
|
||||
peer_addr: SocketAddr,
|
||||
meta: ConnectionMeta,
|
||||
) -> ResultType<String> {
|
||||
let Some(slot) = AnswererSlot::take() else {
|
||||
hbb_common::throttled_log!(
|
||||
ICE_LOG_INTERVAL,
|
||||
warn,
|
||||
"declined a WebRTC offer: {} answerers already in flight",
|
||||
MAX_WEBRTC_ANSWERERS
|
||||
);
|
||||
return Ok(String::new());
|
||||
};
|
||||
let mut stream =
|
||||
WebRTCStream::new(&ph.webrtc_sdp_offer, relay_only_ice, CONNECT_TIMEOUT).await?;
|
||||
let answer = stream.local_endpoint().to_owned();
|
||||
@@ -849,6 +888,11 @@ impl RendezvousMediator {
|
||||
let session_key_for_cleanup = session_key.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = stream.wait_connected(CONNECT_TIMEOUT).await;
|
||||
// The slot covers the setup an unauthenticated offer makes this machine pay for, ICE,
|
||||
// DTLS and SCTP, and that wait is bounded by CONNECT_TIMEOUT. Release it here, before
|
||||
// the cleanup and the close below, so their duration is never added to a slot's life;
|
||||
// with the channel open the session is a connection like any other.
|
||||
drop(slot);
|
||||
// Only evict our own route. The key is the offer's DTLS fingerprint, identical across
|
||||
// the controller's punch retries, so a retry that built a fresh answerer has already
|
||||
// replaced this entry — removing it blindly would delete the live session's sender and
|
||||
@@ -1488,7 +1532,10 @@ impl Drop for CheckIfResendPk {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{mpsc, socket_client, tokio, IceRoute, ICE_DEDUP_WINDOW, MAX_PENDING_REMOTE_ICE};
|
||||
use super::{
|
||||
mpsc, socket_client, tokio, AnswererSlot, IceRoute, ICE_DEDUP_WINDOW,
|
||||
MAX_PENDING_REMOTE_ICE, MAX_WEBRTC_ANSWERERS,
|
||||
};
|
||||
use hbb_common::tcp::new_listener;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
@@ -1736,4 +1783,14 @@ mod tests {
|
||||
"must return when the grace runs out, not a backoff later"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_answerer_slots_cap_and_release() {
|
||||
let held: Vec<_> = (0..MAX_WEBRTC_ANSWERERS)
|
||||
.map(|_| AnswererSlot::take().unwrap())
|
||||
.collect();
|
||||
assert!(AnswererSlot::take().is_none());
|
||||
drop(held);
|
||||
assert!(AnswererSlot::take().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,15 @@ pub struct Server {
|
||||
pub type ServerPtr = Arc<RwLock<Server>>;
|
||||
pub type ServerPtrWeak = Weak<RwLock<Server>>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_for_test() -> ServerPtr {
|
||||
Arc::new(RwLock::new(Server {
|
||||
connections: HashMap::new(),
|
||||
services: HashMap::new(),
|
||||
id_count: 1000,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn new() -> ServerPtr {
|
||||
let mut server = Server {
|
||||
connections: HashMap::new(),
|
||||
@@ -204,7 +213,48 @@ pub async fn create_tcp_connection(
|
||||
meta: ConnectionMeta,
|
||||
) -> ResultType<()> {
|
||||
let mut stream = stream;
|
||||
// The address the connection layer keys on, whitelist and admission alike.
|
||||
let addr = hbb_common::try_into_v4(addr);
|
||||
let id = server.write().unwrap().get_new_id();
|
||||
// Admitted before the identity handshake, so a peer that stalls in it, or after it without
|
||||
// logging in, holds its place the whole time; an address over its share is turned away.
|
||||
let Some(unauthorized) = admit_unauthorized(id, addr.ip()) else {
|
||||
bail!("too many unauthenticated connections from {}", addr.ip());
|
||||
};
|
||||
tokio::select! {
|
||||
handshake = identity_handshake(&mut stream, secure) => handshake?,
|
||||
_ = unauthorized.evicted() => {
|
||||
bail!("evicted to make room for a newer unauthenticated connection");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::process::Command;
|
||||
if let Ok(task) = Command::new("/usr/bin/caffeinate")
|
||||
.arg("-u")
|
||||
.arg("-t 5")
|
||||
.spawn()
|
||||
{
|
||||
super::CHILD_PROCESS.lock().unwrap().push(task);
|
||||
}
|
||||
log::info!("wake up macos");
|
||||
}
|
||||
Connection::start(
|
||||
addr,
|
||||
stream,
|
||||
id,
|
||||
Arc::downgrade(&server),
|
||||
meta,
|
||||
unauthorized,
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Our signed identity goes out and, when `secure`, the controller's reply keys `stream`.
|
||||
/// Separate so it can be raced against the connection's eviction.
|
||||
async fn identity_handshake(stream: &mut Stream, secure: bool) -> ResultType<()> {
|
||||
let (sk, pk) = Config::get_key_pair();
|
||||
if secure && pk.len() == sign::PUBLICKEYBYTES && sk.len() == sign::SECRETKEYBYTES {
|
||||
let mut sk_ = [0u8; sign::SECRETKEYBYTES];
|
||||
@@ -267,19 +317,6 @@ pub async fn create_tcp_connection(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::process::Command;
|
||||
if let Ok(task) = Command::new("/usr/bin/caffeinate")
|
||||
.arg("-u")
|
||||
.arg("-t 5")
|
||||
.spawn()
|
||||
{
|
||||
super::CHILD_PROCESS.lock().unwrap().push(task);
|
||||
}
|
||||
log::info!("wake up macos");
|
||||
}
|
||||
Connection::start(addr, stream, id, Arc::downgrade(&server), meta).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user