Compare commits

..

2 Commits

Author SHA1 Message Date
rustdesk
82d704b2d2 wayland: lang keys for the staged portal failures
Five keys, appended to `template.rs` and to every `src/lang/*.rs`. `it.rs` gets
empty values, as AGENTS.md requires -- it is maintained by hand by its
translator. No `en.rs` entries: each key is already its own English display
text, which is also what an older peer falls back to.

Two carry a `{}`: the portal's response code, and the name of the GStreamer
element that could not be created. `lang.rs`'s `extract_placeholder` resolves a
key by replacing the first `{...}` with `{}`, which is why the server sends the
value still inside the braces and why the scrap side strips braces out of any
detail before it gets there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-08 17:53:59 +08:00
rustdesk
6feb49f060 wayland: say which step of the portal handshake failed
The XDG portal handshake is four sequential requests, and every way it can end
badly -- the user declining, nobody answering, the portal being absent or
ending it or dying mid-handshake, the stream list coming back empty -- left
`request_remote_desktop` through one `bail!` carrying one string.
`map_err_scrap` then guessed a cause by looking for "dbus" or "pipewire" in
that string. Since that string always mentions "PipeWire library", a decline
and a three-minute timeout both came out as "Wayland requires higher version of
linux distro. Please try X11 desktop or change your OS." On Ubuntu 21+, where
the mapping passes the text through untouched, they came out as raw English
pointing at an unrelated GitHub issue.

The response code and the D-Bus error were in hand at the moment of failure and
were being dropped: `handle_response` collapsed all of it into one
`AtomicBool`. Record it instead, tagged with the stage that produced it, and
let the app side look the tag up. `map_err_scrap` gains one leading branch;
anything untagged -- which is everything the capture loop reports -- takes the
existing path unchanged.

The tag decides between five new keys and the existing `xdp-portal-unavailable`:

- A decline, nobody answering, the portal ending the request with its response
  code, an empty stream list, and a missing GStreamer element each get their
  own message. Sentence-case English, so a peer that has never heard of them
  falls back to the key and still reads as a sentence rather than showing a
  slug like `x11 expected`.
- Everything else is the portal failing to deliver, which is what
  `xdp-portal-unavailable` already says -- it is already translated everywhere
  and carries the one remedy a user can act on, `systemctl --user restart
  xdg-desktop-portal`. The D-Bus error name and message go to the log.
- The Ubuntu-before-21 branch keeps every outcome that says something about the
  machine and yields the two that say what a person did.

`fill_displays` needs the tag resolved at its own call site: it opens a second
portal session for cursor-based display disambiguation, and its error returns
straight up `check_init` without passing through `map_err_scrap`, so a tag
would otherwise reach the peer verbatim.

Two existing paths change, both necessarily:

- `check_init` no longer wraps `Capturer::new` in `with_context`. The peer is
  shown `format!("{}", err)` (connection.rs), which renders only the outermost
  layer, so that context was replacing the mapped code with "Failed to create
  capturer for display 0".
- The `std::process::exit(-1)` on libdbus' no-reply text is now reached only by
  the capture loop, which is what that self-heal was written for. A no-reply
  during the handshake is tagged and reported instead. It is worth saying
  plainly what that branch did before: the portal proxy has a one-second
  timeout, so a portal slow to activate could take the whole service down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-08 17:53:59 +08:00
106 changed files with 705 additions and 8886 deletions

View File

@@ -1,42 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
app_path=$1
identity=$2
entitlements=$3
sign_args=(--force --options runtime --sign "$identity")
if [[ "$identity" != "-" ]]; then
sign_args+=(--timestamp)
fi
frameworks_path="$app_path/Contents/Frameworks"
if [[ -d "$frameworks_path" ]]; then
while IFS= read -r -d '' code; do
if file -b "$code" | grep -q 'Mach-O'; then
codesign "${sign_args[@]}" "$code"
fi
done < <(find "$frameworks_path" -type f -print0)
while IFS= read -r -d '' framework; do
codesign "${sign_args[@]}" "$framework"
done < <(find "$frameworks_path" -depth -type d -name '*.framework' -print0)
fi
service_path="$app_path/Contents/MacOS/service"
if [[ -f "$service_path" ]]; then
codesign "${sign_args[@]}" "$service_path"
fi
codesign "${sign_args[@]}" --generate-entitlement-der \
--entitlements "$entitlements" "$app_path"
codesign --verify --deep --strict --verbose=2 "$app_path"
actual_entitlements=$(codesign -d --entitlements :- "$app_path" 2>/dev/null)
audio_input=$(plutil -extract 'com\.apple\.security\.device\.audio-input' raw - \
<<<"$actual_entitlements")
if [[ "$audio_input" != "true" ]]; then
echo "Missing com.apple.security.device.audio-input entitlement" >&2
exit 1
fi

View File

@@ -374,12 +374,6 @@ jobs:
- name: Add MSBuild to PATH - name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2 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 - name: Build msi
# Builds the MSI for the matrix arch. res/msi (WiX v4 + native CustomActions) carries # Builds the MSI for the matrix arch. res/msi (WiX v4 + native CustomActions) carries
@@ -498,20 +492,11 @@ jobs:
version: ${{ env.LLVM_VERSION }} version: ${{ env.LLVM_VERSION }}
- name: Install Rust toolchain - name: Install Rust toolchain
shell: bash uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
run: | with:
# Sciter's abi_thiscall feature requires nightly Rust. toolchain: nightly-2023-10-13-${{ matrix.job.target }} # must use nightly here, because of abi_thiscall feature required
# Use an i686 host toolchain so build scripts can load the 32-bit LLVM installed above. targets: ${{ matrix.job.target }}
# Since rustup 1.29.1, i686 toolchains on x64 Windows require --force-non-host components: "rustfmt"
# 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 - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with: with:
@@ -941,11 +926,7 @@ jobs:
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg # start sign the rustdesk.app and dmg
rm -rf *.dmg || true rm -rf *.dmg || true
# the identity secret carries its own shell quoting, so expand it inline like the dmg codesign below codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
bash ./.github/scripts/sign-macos-app.sh \
./flutter/build/macos/Build/Products/Release/RustDesk.app \
${{ 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 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 codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg # notarize the rustdesk-${{ env.VERSION }}.dmg

21
Cargo.lock generated
View File

@@ -985,9 +985,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.1" version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
dependencies = [ dependencies = [
"serde 1.0.228", "serde 1.0.228",
] ]
@@ -1717,7 +1717,7 @@ dependencies = [
[[package]] [[package]]
name = "cpal" name = "cpal"
version = "0.15.3" 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#6b374bcaed076750ca8fce6da518ab39b882e14a"
dependencies = [ dependencies = [
"alsa", "alsa",
"cidre", "cidre",
@@ -1791,9 +1791,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-epoch" name = "crossbeam-epoch"
version = "0.9.20" version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
@@ -4280,7 +4280,7 @@ dependencies = [
[[package]] [[package]]
name = "kcp-sys" name = "kcp-sys"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/rustdesk-org/kcp-sys?branch=rustdesk-patches#938eda3e5e9757a612385503af7a6cb1189b2cdd" source = "git+https://github.com/rustdesk-org/kcp-sys?branch=rustdesk-patches#023a0065398968989f2ddfcf5cc72bb886d02675"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"auto_impl", "auto_impl",
@@ -6496,9 +6496,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn-proto" name = "quinn-proto"
version = "0.11.15" version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [ dependencies = [
"bytes", "bytes",
"getrandom 0.3.2", "getrandom 0.3.2",
@@ -7139,7 +7139,6 @@ dependencies = [
"lazy_static", "lazy_static",
"libpulse-binding", "libpulse-binding",
"libpulse-simple-binding", "libpulse-simple-binding",
"libsamplerate-sys",
"libxdo-sys", "libxdo-sys",
"mac_address", "mac_address",
"magnum-opus", "magnum-opus",
@@ -9732,7 +9731,7 @@ dependencies = [
[[package]] [[package]]
name = "webrtc-sctp" name = "webrtc-sctp"
version = "0.12.0" version = "0.12.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=db3b07a9dd8f195916c89c2e62a8911402b11d27#db3b07a9dd8f195916c89c2e62a8911402b11d27" source = "git+https://github.com/rustdesk-org/webrtc?rev=b221f13b1d6f21fbce09f9f63096be27dd392265#b221f13b1d6f21fbce09f9f63096be27dd392265"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-trait", "async-trait",
@@ -9772,7 +9771,7 @@ dependencies = [
[[package]] [[package]]
name = "webrtc-util" name = "webrtc-util"
version = "0.11.0" version = "0.11.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=db3b07a9dd8f195916c89c2e62a8911402b11d27#db3b07a9dd8f195916c89c2e62a8911402b11d27" source = "git+https://github.com/rustdesk-org/webrtc?rev=b221f13b1d6f21fbce09f9f63096be27dd392265#b221f13b1d6f21fbce09f9f63096be27dd392265"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bitflags 1.3.2", "bitflags 1.3.2",

View File

@@ -22,7 +22,7 @@ path = "src/service.rs"
[features] [features]
inline = [] inline = []
use_samplerate = ["samplerate", "libsamplerate-sys"] use_samplerate = ["samplerate"]
use_rubato = ["rubato"] use_rubato = ["rubato"]
use_dasp = ["dasp"] use_dasp = ["dasp"]
flutter = ["flutter_rust_bridge"] flutter = ["flutter_rust_bridge"]
@@ -67,7 +67,6 @@ magnum-opus = { git = "https://github.com/rustdesk-org/magnum-opus" }
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true } dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
rubato = { version = "0.12", optional = true } rubato = { version = "0.12", optional = true }
samplerate = { version = "0.2", optional = true } samplerate = { version = "0.2", optional = true }
libsamplerate-sys = { version = "0.1.12", optional = true }
uuid = { version = "1.3", features = ["v4"] } uuid = { version = "1.3", features = ["v4"] }
num_cpus = "1.15" num_cpus = "1.15"
bytes = { version = "1.4", features = ["serde"] } bytes = { version = "1.4", features = ["serde"] }
@@ -231,8 +230,8 @@ 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 # 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. # 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. # 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-util = { git = "https://github.com/rustdesk-org/webrtc", rev = "b221f13b1d6f21fbce09f9f63096be27dd392265" }
webrtc-sctp = { git = "https://github.com/rustdesk-org/webrtc", rev = "db3b07a9dd8f195916c89c2e62a8911402b11d27" } webrtc-sctp = { git = "https://github.com/rustdesk-org/webrtc", rev = "b221f13b1d6f21fbce09f9f63096be27dd392265" }
[package.metadata.winres] [package.metadata.winres]
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved." LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."

View File

@@ -1,11 +0,0 @@
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.

View File

@@ -1 +0,0 @@
Aplicación de acceso remoto de código abierto, alternativa a TeamViewer.

View File

@@ -1,11 +0,0 @@
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.

View File

@@ -1 +0,0 @@
Aplicativo de acesso remoto open source, alternativa ao TeamViewer.

View File

@@ -178,7 +178,6 @@ const String kOptionEnableIpv6Punch = "enable-ipv6-punch";
const String kOptionAllowSyncClipboardBetweenSessions = const String kOptionAllowSyncClipboardBetweenSessions =
"allow-sync-clipboard-between-sessions"; "allow-sync-clipboard-between-sessions";
const String kOptionEnableWebrtc = "enable-webrtc"; const String kOptionEnableWebrtc = "enable-webrtc";
const String kOptionRelayFallbackDelay = "relay-fallback-delay";
const String kOptionEnableTrustedDevices = "enable-trusted-devices"; const String kOptionEnableTrustedDevices = "enable-trusted-devices";
const String kOptionShowVirtualMouse = "show-virtual-mouse"; const String kOptionShowVirtualMouse = "show-virtual-mouse";
const String kOptionVirtualMouseScale = "virtual-mouse-scale"; const String kOptionVirtualMouseScale = "virtual-mouse-scale";

View File

@@ -591,7 +591,13 @@ class _GeneralState extends State<_General> {
isServer: false, isServer: false,
), ),
], ],
if (!incomingOnly) ...webrtcOptions(context), if (!incomingOnly)
_OptionCheckBox(
context,
'Enable WebRTC P2P connection',
kOptionEnableWebrtc,
isServer: false,
),
if (!isWeb && !incomingOnly) if (!isWeb && !incomingOnly)
Tooltip( Tooltip(
message: translate('sync-clipboard-between-sessions-tip'), message: translate('sync-clipboard-between-sessions-tip'),
@@ -881,85 +887,6 @@ class _GeneralState extends State<_General> {
).marginOnly(left: _kContentHMargin); ).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 { enum _AccessMode {

View File

@@ -896,13 +896,9 @@ class FfiModel with ChangeNotifier {
final text = evt['text']; final text = evt['text'];
final link = evt['link']; final link = evt['link'];
// The peer-gone detector reconnects under `restarting-show` rather than an error title, so
// it needs naming here too. By its own title, not the type: an explicitly restarted remote
// device reaches the same type from a path this change does not touch.
if (isAndroid && if (isAndroid &&
_androidDocumentPickerActive && _androidDocumentPickerActive &&
(title == 'Connection Error' || title == 'Connection Error') {
(type == 'restarting-show' && title == 'Connecting...'))) {
_androidDocumentPickerInterruptedConnection = true; _androidDocumentPickerInterruptedConnection = true;
return; return;
} }
@@ -3601,11 +3597,9 @@ class QualityMonitorModel with ChangeNotifier {
bool get show => _show; bool get show => _show;
QualityMonitorData get data => _data; QualityMonitorData get data => _data;
// Only a WebRTC session on the web names its transport here: web has no // Only a WebRTC session names its transport here: web has no session tab
// session tab to show it on (the desktop tab's tooltip already does), and // to show it on, and WebRTC is the one path that can be direct or TURN.
// WebRTC is the one path that can be direct or TURN.
String? get webrtcTransport { String? get webrtcTransport {
if (!isWeb) return null;
final ffiModel = parent.target?.ffiModel; final ffiModel = parent.target?.ffiModel;
if (ffiModel == null) return null; if (ffiModel == null) return null;
final streamType = ffiModel.cachedPeerData.streamType; final streamType = ffiModel.cachedPeerData.streamType;

View File

@@ -306,7 +306,7 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: bd6b5b41254e57c5bcece202ebfb234de63e6487 ref: HEAD
resolved-ref: bd6b5b41254e57c5bcece202ebfb234de63e6487 resolved-ref: bd6b5b41254e57c5bcece202ebfb234de63e6487
url: "https://github.com/rustdesk-org/Dash-Chat-2" url: "https://github.com/rustdesk-org/Dash-Chat-2"
source: git source: git
@@ -339,8 +339,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "8b774a66671cbb9bcb2631af6ac28f9bdd469ce3" ref: HEAD
resolved-ref: "8b774a66671cbb9bcb2631af6ac28f9bdd469ce3" resolved-ref: 8b774a66671cbb9bcb2631af6ac28f9bdd469ce3
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
source: git source: git
version: "0.1.0" version: "0.1.0"
@@ -1581,7 +1581,7 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: cf4aef0512092fad9344a27ffe1c47ad83269dfc ref: HEAD
resolved-ref: cf4aef0512092fad9344a27ffe1c47ad83269dfc resolved-ref: cf4aef0512092fad9344a27ffe1c47ad83269dfc
url: "https://github.com/rustdesk-org/window_manager" url: "https://github.com/rustdesk-org/window_manager"
source: git source: git

View File

@@ -40,7 +40,6 @@ dependencies:
dash_chat_2: dash_chat_2:
git: git:
url: https://github.com/rustdesk-org/Dash-Chat-2 url: https://github.com/rustdesk-org/Dash-Chat-2
ref: bd6b5b41254e57c5bcece202ebfb234de63e6487
draggable_float_widget: ^0.1.0 draggable_float_widget: ^0.1.0
settings_ui: ^2.0.2 settings_ui: ^2.0.2
flutter_breadcrumb: ^1.0.1 flutter_breadcrumb: ^1.0.1
@@ -54,11 +53,9 @@ dependencies:
window_manager: window_manager:
git: git:
url: https://github.com/rustdesk-org/window_manager url: https://github.com/rustdesk-org/window_manager
ref: cf4aef0512092fad9344a27ffe1c47ad83269dfc
desktop_multi_window: desktop_multi_window:
git: git:
url: https://github.com/rustdesk-org/rustdesk_desktop_multi_window url: https://github.com/rustdesk-org/rustdesk_desktop_multi_window
ref: 8b774a66671cbb9bcb2631af6ac28f9bdd469ce3
freezed_annotation: ^2.0.3 freezed_annotation: ^2.0.3
flutter_custom_cursor: flutter_custom_cursor:
git: git:

View File

@@ -1,28 +0,0 @@
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);
});
}

View File

@@ -129,7 +129,6 @@ pub const OPTION_ENABLE_UDP_PUNCH: &str = "enable-udp-punch";
pub const OPTION_ENABLE_IPV6_PUNCH: &str = "enable-ipv6-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_PORT_FORWARD_MUX: &str = "enable-port-forward-mux";
pub const OPTION_ENABLE_WEBRTC: &str = "enable-webrtc"; 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_ALLOW_KCP_CC: &str = "allow-kcp-congestion-control";
pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card"; pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card";
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards"; pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
@@ -259,7 +258,6 @@ pub const KEYS_LOCAL_SETTINGS: &[&str] = &[
OPTION_ENABLE_IPV6_PUNCH, OPTION_ENABLE_IPV6_PUNCH,
OPTION_ENABLE_PORT_FORWARD_MUX, OPTION_ENABLE_PORT_FORWARD_MUX,
OPTION_ENABLE_WEBRTC, OPTION_ENABLE_WEBRTC,
OPTION_RELAY_FALLBACK_DELAY,
OPTION_TOUCH_MODE, OPTION_TOUCH_MODE,
OPTION_SHOW_VIRTUAL_MOUSE, OPTION_SHOW_VIRTUAL_MOUSE,
OPTION_SHOW_VIRTUAL_JOYSTICK, OPTION_SHOW_VIRTUAL_JOYSTICK,

View File

@@ -265,13 +265,10 @@ pub struct PipeWireRecorder {
} }
// Element creation fails the same way for a plugin that is not installed as for one that is // Element creation fails the same way for a plugin that is not installed as for one that is
// broken, so the tag does not claim which. Only the name travels to the peer -- it is what // broken, and the name is the only thing that tells a user which package to look at.
// says which package to look at -- and the factory's own error stays here in the log.
fn gst_element(name: &str) -> ResultType<gst::Element> { fn gst_element(name: &str) -> ResultType<gst::Element> {
gst::ElementFactory::make(name, None).map_err(|e| { gst::ElementFactory::make(name, None)
error!("Failed to create GStreamer element {}: {}", name, e); .map_err(|_| anyhow!(stage_err("gst-plugin", "missing", name)))
anyhow!(stage_err("gst-plugin", "unavailable", name))
})
} }
impl PipeWireRecorder { impl PipeWireRecorder {
@@ -484,7 +481,6 @@ enum PortalStage {
SelectDevices = 2, SelectDevices = 2,
SelectSources = 3, SelectSources = 3,
Start = 4, Start = 4,
OpenPipeWireRemote = 5,
} }
impl PortalStage { impl PortalStage {
@@ -494,7 +490,6 @@ impl PortalStage {
Self::SelectDevices => "select-devices", Self::SelectDevices => "select-devices",
Self::SelectSources => "select-sources", Self::SelectSources => "select-sources",
Self::Start => "start", Self::Start => "start",
Self::OpenPipeWireRemote => "open-pipewire-remote",
} }
} }
@@ -503,7 +498,6 @@ impl PortalStage {
2 => Self::SelectDevices, 2 => Self::SelectDevices,
3 => Self::SelectSources, 3 => Self::SelectSources,
4 => Self::Start, 4 => Self::Start,
5 => Self::OpenPipeWireRemote,
_ => Self::CreateSession, _ => Self::CreateSession,
} }
} }
@@ -512,8 +506,6 @@ impl PortalStage {
// `wl-stage:<stage>:<kind>:<detail>`, parsed by `map_err_scrap` on the app side. The detail // `wl-stage:<stage>:<kind>:<detail>`, parsed by `map_err_scrap` on the app side. The detail
// reaches the user through a `{}` placeholder in a translated string, so it must not bring // reaches the user through a `{}` placeholder in a translated string, so it must not bring
// braces, control characters or unbounded length of its own. // braces, control characters or unbounded length of its own.
const STAGE_TAG: &str = "wl-stage:";
fn stage_err(stage: &str, kind: &str, detail: &str) -> String { fn stage_err(stage: &str, kind: &str, detail: &str) -> String {
let detail: String = detail let detail: String = detail
.chars() .chars()
@@ -521,25 +513,17 @@ fn stage_err(stage: &str, kind: &str, detail: &str) -> String {
.filter(|c| *c != '{' && *c != '}') .filter(|c| *c != '{' && *c != '}')
.take(200) .take(200)
.collect(); .collect();
format!("{}{}:{}:{}", STAGE_TAG, stage, kind, detail.trim()) format!("wl-stage:{}:{}:{}", stage, kind, detail.trim())
} }
// The name alone is usually the generic `org.freedesktop.DBus.Error.Failed`; the message is fn dbus_detail(err: &dbus::Error) -> String {
// where a backend says what it objected to. This ends up in the log, so carry both. match (err.name(), err.message()) {
fn dbus_stage_err(stage: &str, err: &dbus::Error) -> String {
let detail = match (err.name(), err.message()) {
(Some(name), Some(message)) if !name.is_empty() && !message.is_empty() => { (Some(name), Some(message)) if !name.is_empty() && !message.is_empty() => {
format!("{}: {}", name, message) format!("{}: {}", name, message)
} }
(Some(name), _) if !name.is_empty() => name.to_owned(), (Some(name), _) if !name.is_empty() => name.to_owned(),
(_, message) => message.unwrap_or_default().to_owned(), (_, message) => message.unwrap_or_default().to_owned(),
}; }
let kind = match err.name().unwrap_or_default() {
"org.freedesktop.DBus.Error.UnknownMethod"
| "org.freedesktop.DBus.Error.UnknownInterface" => "unsupported",
_ => "dbus",
};
stage_err(stage, kind, &detail)
} }
#[derive(Clone)] #[derive(Clone)]
@@ -618,11 +602,6 @@ where
trace.fail(stage, "declined", ""); trace.fail(stage, "declined", "");
return true; return true;
} }
2 => {
warn!("DBus response: User interaction ended in some other way.");
trace.fail(stage, "ended", "");
return true;
}
c => { c => {
warn!("DBus response: Unknown error, code: {}.", c); warn!("DBus response: Unknown error, code: {}.", c);
trace.fail(stage, "portal-error", &c.to_string()); trace.fail(stage, "portal-error", &c.to_string());
@@ -630,14 +609,8 @@ where
} }
} }
if let Err(err) = f(r, c, m) { if let Err(err) = f(r, c, m) {
let text = err.to_string(); warn!("Error requesting screen capture via dbus: {}", err);
warn!("Error requesting screen capture via dbus: {}", text); trace.fail(trace.waiting_stage(), "internal", &err.to_string());
if text.starts_with(STAGE_TAG) {
trace.record(text);
trace.failed.store(true, Ordering::SeqCst);
} else {
trace.fail(trace.waiting_stage(), "internal", &text);
}
} }
true true
}) })
@@ -773,8 +746,8 @@ pub fn request_remote_desktop(
INIT = true; INIT = true;
} }
} }
let conn = let conn = SyncConnection::new_session()
SyncConnection::new_session().map_err(|e| anyhow!(dbus_stage_err("session-bus", &e)))?; .map_err(|e| anyhow!(stage_err("session-bus", "dbus", &dbus_detail(&e))))?;
let portal = get_portal(&conn); let portal = get_portal(&conn);
let mut args: PropMap = HashMap::new(); let mut args: PropMap = HashMap::new();
let fd: Arc<Mutex<Option<OwnedFd>>> = Arc::new(Mutex::new(None)); let fd: Arc<Mutex<Option<OwnedFd>>> = Arc::new(Mutex::new(None));
@@ -810,8 +783,7 @@ pub fn request_remote_desktop(
// the caller to subscribe to the signal before making the method call. // the caller to subscribe to the signal before making the method call.
handle_response( handle_response(
&conn, &conn,
get_request_path(&conn, create_session_handle_token) get_request_path(&conn, create_session_handle_token)?,
.map_err(|e| anyhow!(dbus_stage_err("create-session", &e)))?,
on_create_session_response( on_create_session_response(
fd.clone(), fd.clone(),
streams.clone(), streams.clone(),
@@ -822,20 +794,18 @@ pub fn request_remote_desktop(
), ),
trace.clone(), trace.clone(),
PortalStage::CreateSession, PortalStage::CreateSession,
) )?;
.map_err(|e| anyhow!(dbus_stage_err("create-session", &e)))?;
if is_server_running() { if is_server_running() {
let _ = screencast_portal::create_session(&portal, args) let _ = screencast_portal::create_session(&portal, args)
.map_err(|e| anyhow!(dbus_stage_err("create-session", &e)))?; .map_err(|e| anyhow!(stage_err("create-session", "dbus", &dbus_detail(&e))))?;
} else { } else {
let _ = remote_desktop_portal::create_session(&portal, args) let _ = remote_desktop_portal::create_session(&portal, args)
.map_err(|e| anyhow!(dbus_stage_err("create-session", &e)))?; .map_err(|e| anyhow!(stage_err("create-session", "dbus", &dbus_detail(&e))))?;
} }
// wait 3 minutes for user interaction // wait 3 minutes for user interaction
for _ in 0..1800 { for _ in 0..1800 {
conn.process(Duration::from_millis(100)) conn.process(Duration::from_millis(100))?;
.map_err(|e| anyhow!(dbus_stage_err(trace_res.waiting_stage().as_str(), &e)))?;
// Once we got a file descriptor we are done! // Once we got a file descriptor we are done!
if fd_res.lock().unwrap().is_some() { if fd_res.lock().unwrap().is_some() {
break; break;
@@ -936,7 +906,6 @@ fn on_create_session_response(
}); });
} }
trace.waiting(PortalStage::SelectSources);
handle_response( handle_response(
c, c,
get_request_path(c, select_sources_handle_token)?, get_request_path(c, select_sources_handle_token)?,
@@ -950,9 +919,8 @@ fn on_create_session_response(
trace.clone(), trace.clone(),
PortalStage::SelectSources, PortalStage::SelectSources,
)?; )?;
let _ = portal trace.waiting(PortalStage::SelectSources);
.select_sources(ses.clone(), args) let _ = portal.select_sources(ses.clone(), args)?;
.map_err(|e| DBusError(dbus_stage_err("select-sources", &e)))?;
} else { } else {
// TODO: support persist_mode for remote_desktop_portal // TODO: support persist_mode for remote_desktop_portal
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html
@@ -964,7 +932,6 @@ fn on_create_session_response(
); );
args.insert("types".to_string(), Variant(Box::new(7u32))); args.insert("types".to_string(), Variant(Box::new(7u32)));
trace.waiting(PortalStage::SelectDevices);
handle_response( handle_response(
c, c,
get_request_path(c, select_devices_handle_token)?, get_request_path(c, select_devices_handle_token)?,
@@ -978,9 +945,8 @@ fn on_create_session_response(
trace.clone(), trace.clone(),
PortalStage::SelectDevices, PortalStage::SelectDevices,
)?; )?;
let _ = portal trace.waiting(PortalStage::SelectDevices);
.select_devices(ses.clone(), args) let _ = portal.select_devices(ses.clone(), args)?;
.map_err(|e| DBusError(dbus_stage_err("select-devices", &e)))?;
} }
Ok(()) Ok(())
@@ -1013,7 +979,6 @@ fn on_select_devices_response(
args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32))); args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32)));
let session = session.clone(); let session = session.clone();
trace.waiting(PortalStage::SelectSources);
handle_response( handle_response(
c, c,
get_request_path(c, select_sources_handle_token)?, get_request_path(c, select_sources_handle_token)?,
@@ -1027,9 +992,8 @@ fn on_select_devices_response(
trace.clone(), trace.clone(),
PortalStage::SelectSources, PortalStage::SelectSources,
)?; )?;
let _ = portal trace.waiting(PortalStage::SelectSources);
.select_sources(session.clone(), args) let _ = portal.select_sources(session.clone(), args)?;
.map_err(|e| DBusError(dbus_stage_err("select-sources", &e)))?;
Ok(()) Ok(())
} }
@@ -1054,7 +1018,6 @@ fn on_select_sources_response(
"handle_token".to_string(), "handle_token".to_string(),
Variant(Box::new(start_handle_token.to_string())), Variant(Box::new(start_handle_token.to_string())),
); );
trace.waiting(PortalStage::Start);
handle_response( handle_response(
c, c,
get_request_path(c, start_handle_token)?, get_request_path(c, start_handle_token)?,
@@ -1062,18 +1025,16 @@ fn on_select_sources_response(
fd.clone(), fd.clone(),
streams.clone(), streams.clone(),
session.clone(), session.clone(),
trace.clone(),
is_support_restore_token, is_support_restore_token,
), ),
trace.clone(), trace.clone(),
PortalStage::Start, PortalStage::Start,
)?; )?;
trace.waiting(PortalStage::Start);
if is_server_running() { if is_server_running() {
let _ = screencast_portal::start(&portal, session.clone(), "", args) let _ = screencast_portal::start(&portal, session.clone(), "", args)?;
.map_err(|e| DBusError(dbus_stage_err("start", &e)))?;
} else { } else {
let _ = remote_desktop_portal::start(&portal, session.clone(), "", args) let _ = remote_desktop_portal::start(&portal, session.clone(), "", args)?;
.map_err(|e| DBusError(dbus_stage_err("start", &e)))?;
} }
Ok(()) Ok(())
@@ -1084,7 +1045,6 @@ fn on_start_response(
fd: Arc<Mutex<Option<OwnedFd>>>, fd: Arc<Mutex<Option<OwnedFd>>>,
streams: Arc<Mutex<Vec<PwStreamInfo>>>, streams: Arc<Mutex<Vec<PwStreamInfo>>>,
session: dbus::Path<'static>, session: dbus::Path<'static>,
trace: PortalTrace,
is_support_restore_token: bool, is_support_restore_token: bool,
) -> impl Fn( ) -> impl Fn(
OrgFreedesktopPortalRequestResponse, OrgFreedesktopPortalRequestResponse,
@@ -1112,14 +1072,10 @@ fn on_start_response(
.lock() .lock()
.unwrap() .unwrap()
.append(&mut streams_from_response(r)); .append(&mut streams_from_response(r));
// Past this point the user has granted the request; anything that fails now is the fd.clone()
// hand-over of the PipeWire fd, which is a different thing to go looking at. .lock()
trace.waiting(PortalStage::OpenPipeWireRemote); .unwrap()
fd.clone().lock().unwrap().replace( .replace(portal.open_pipe_wire_remote(session.clone(), HashMap::new())?);
portal
.open_pipe_wire_remote(session.clone(), HashMap::new())
.map_err(|e| DBusError(dbus_stage_err("open-pipewire-remote", &e)))?,
);
Ok(()) Ok(())
} }

View File

@@ -1,242 +0,0 @@
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;

View File

@@ -1,142 +0,0 @@
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());
}
}
}
}

View File

@@ -1,157 +0,0 @@
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,
}
);
}

View File

@@ -1,112 +0,0 @@
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)
))
}

View File

@@ -1,178 +0,0 @@
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"
);
}

View File

@@ -93,10 +93,6 @@ use crate::ui_session_interface::SessionPermissionConfig;
pub use super::lang::*; pub use super::lang::*;
#[cfg(not(target_os = "linux"))]
mod audio_playback;
#[cfg(all(test, not(target_os = "linux")))]
mod audio_state_tests;
pub mod file_trait; pub mod file_trait;
pub mod helper; pub mod helper;
pub mod io_loop; pub mod io_loop;
@@ -573,7 +569,7 @@ impl Client {
return race_transports_prefer_webrtc( return race_transports_prefer_webrtc(
preferred_fut, preferred_fut,
vec![fallback_fut], vec![fallback_fut],
Self::relay_fallback_delay_ms(), Self::WEBRTC_PREFER_WINDOW_MS,
|result| result.0 .1, |result| result.0 .1,
) )
.await; .await;
@@ -614,27 +610,11 @@ impl Client {
/// ones that traverse NAT. /// ones that traverse NAT.
const MAX_PENDING_WEBRTC_ICE: usize = 64; const MAX_PENDING_WEBRTC_ICE: usize = 64;
/// Default relay fallback delay: how long an already-established relay result is held back /// Prefer-P2P window: how long a WebRTC attempt outranks an already-established relay
/// while a WebRTC attempt is still in flight, and the floor for a punch-path WebRTC attempt /// result, and the floor for a punch-path WebRTC attempt whose race timeout is tuned for a
/// whose race timeout is tuned for a raw TCP SYN. Long enough for candidate trickle + ICE /// raw TCP SYN. Long enough for candidate trickle + ICE checks + DTLS on high-latency
/// checks + DTLS on high-latency links; short enough that UDP-blocked networks settle on /// links; short enough that UDP-blocked networks settle on relay without a noticeable wait.
/// relay without a noticeable wait. The same role RFC 8305 calls a connection attempt delay. const WEBRTC_PREFER_WINDOW_MS: u64 = 2500;
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 /// 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 /// normal bound is `rtt / 2`: the test has been running since before the TCP connect, so on
@@ -1134,7 +1114,7 @@ impl Client {
race_transports_prefer_webrtc( race_transports_prefer_webrtc(
webrtc_fut, webrtc_fut,
connect_futures, connect_futures,
Self::relay_fallback_delay_ms(), Self::WEBRTC_PREFER_WINDOW_MS,
|result| result.3, |result| result.3,
) )
.await .await
@@ -1462,7 +1442,7 @@ impl Client {
// so a viable P2P path is not abandoned before it can complete; TCP/UDP keep the // 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 // tighter timeout, so a working direct connection still wins immediately, and the
// relay fallback only waits the extra time when direct attempts all failed. // relay fallback only waits the extra time when direct attempts all failed.
let webrtc_timeout = connect_timeout.max(Self::relay_fallback_delay_ms()); let webrtc_timeout = connect_timeout.max(Self::WEBRTC_PREFER_WINDOW_MS);
async move { async move {
raced.wait_connected(webrtc_timeout).await?; raced.wait_connected(webrtc_timeout).await?;
// Resolve the pair here: a TURN win is relayed, not direct, and must be held // Resolve the pair here: a TURN win is relayed, not direct, and must be held
@@ -1480,7 +1460,7 @@ impl Client {
race_transports_prefer_webrtc( race_transports_prefer_webrtc(
webrtc_fut, webrtc_fut,
direct_futures, direct_futures,
Self::relay_fallback_delay_ms(), Self::WEBRTC_PREFER_WINDOW_MS,
|r| r.3, |r| r.3,
) )
.await .await
@@ -2073,8 +2053,6 @@ pub struct AudioHandler {
simple: Option<psimple::Simple>, simple: Option<psimple::Simple>,
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
audio_buffer: AudioBuffer, audio_buffer: AudioBuffer,
#[cfg(not(target_os = "linux"))]
audio_resampler: Option<crate::audio_resampler::AudioResampler>,
sample_rate: (u32, u32), sample_rate: (u32, u32),
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
audio_stream: Option<Box<dyn StreamTrait>>, audio_stream: Option<Box<dyn StreamTrait>>,
@@ -2082,55 +2060,7 @@ pub struct AudioHandler {
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
device_channel: u16, device_channel: u16,
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
playback_status: Arc<audio_playback::AudioPlaybackStatus>, ready: Arc<std::sync::Mutex<bool>>,
}
#[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"))] #[cfg(not(target_os = "linux"))]
@@ -2138,7 +2068,6 @@ struct AudioBuffer(
pub Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>, pub Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>,
usize, usize,
[usize; 30], [usize; 30],
Arc<std::sync::atomic::AtomicUsize>,
); );
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
@@ -2150,7 +2079,6 @@ impl Default for AudioBuffer {
)), )),
48000 * 2, 48000 * 2,
[0; 30], [0; 30],
Arc::new(std::sync::atomic::AtomicUsize::new(0)),
) )
} }
} }
@@ -2225,36 +2153,27 @@ impl AudioBuffer {
let skip = (cap * max / (30 * N) + 1) & (!1); let skip = (cap * max / (30 * N) + 1) & (!1);
if (having > skip * 3) && (skip > 0) { if (having > skip * 3) && (skip > 0) {
lock.skip(skip); lock.skip(skip);
let generation = self.signal_discontinuity(); log::info!("skip {skip}, based {max} {zero}");
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 /// append pcm to audio buffer, if buffered data
/// exceeds AUDIO_BUFFER_MS, only AUDIO_BUFFER_MS /// exceeds AUDIO_BUFFER_MS, only AUDIO_BUFFER_MS
/// will be kept. /// will be kept.
fn append_pcm2(&self, buffer: &[f32]) -> usize { fn append_pcm2(&self, buffer: &[f32]) -> usize {
let mut lock = self.0.lock().unwrap(); let mut lock = self.0.lock().unwrap();
let cap = lock.capacity(); let cap = lock.capacity();
let having = lock.occupied_len() + buffer.len(); if buffer.len() > cap {
lock.push_slice_overwrite(buffer); lock.push_slice_overwrite(buffer);
let discard = (having > cap).then(|| (having - cap, self.signal_discontinuity())); return cap;
let occupied = lock.occupied_len();
drop(lock);
if let Some((discarded, generation)) = discard {
log::debug!(
"Audio buffer capacity discard: samples={discarded}, generation={generation}"
);
} }
occupied
let having = lock.occupied_len() + buffer.len();
if having > cap {
lock.skip(having - cap);
}
lock.push_slice_overwrite(buffer);
lock.occupied_len()
} }
/// append pcm to audio buffer, trying to drop data /// append pcm to audio buffer, trying to drop data
@@ -2266,41 +2185,6 @@ 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 { impl AudioHandler {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> {
@@ -2354,9 +2238,6 @@ impl AudioHandler {
} }
self.sample_rate = (format0.sample_rate, config.sample_rate.0); 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 { let mut build_output_stream = |config: StreamConfig| match sample_format {
cpal::SampleFormat::I8 => self.build_output_stream::<i8>(&config, &device), cpal::SampleFormat::I8 => self.build_output_stream::<i8>(&config, &device),
cpal::SampleFormat::I16 => self.build_output_stream::<i16>(&config, &device), cpal::SampleFormat::I16 => self.build_output_stream::<i16>(&config, &device),
@@ -2381,7 +2262,6 @@ impl AudioHandler {
} else { } else {
build_output_stream(config)?; build_output_stream(config)?;
} }
self.audio_resampler = audio_resampler;
Ok(()) Ok(())
} }
@@ -2394,17 +2274,10 @@ impl AudioHandler {
} }
match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) { match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) {
Ok(d) => { Ok(d) => {
#[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 = false;
let buffer = vec![0.; f.sample_rate as usize * f.channels as usize]; let buffer = vec![0.; f.sample_rate as usize * f.channels as usize];
self.audio_decoder = Some((d, buffer)); self.audio_decoder = Some((d, buffer));
self.channels = f.channels as _; self.channels = f.channels as _;
let result = self.start_audio(f); allow_err!(self.start_audio(f));
self.handle_audio_start_result(result, keep_existing_stream);
} }
Err(err) => { Err(err) => {
log::error!("Failed to create audio decoder: {}", err); log::error!("Failed to create audio decoder: {}", err);
@@ -2412,31 +2285,11 @@ 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. /// Handle audio frame and play it.
#[inline] #[inline]
pub fn handle_frame(&mut self, frame: AudioFrame) { pub fn handle_frame(&mut self, frame: AudioFrame) {
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
self.playback_status.report_errors(); if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() {
#[cfg(not(target_os = "linux"))]
if self.audio_stream.is_none()
|| !self
.playback_status
.ready
.load(std::sync::atomic::Ordering::Acquire)
{
return; return;
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -2445,40 +2298,39 @@ impl AudioHandler {
return; return;
} }
self.audio_decoder.as_mut().map(|(d, buffer)| { self.audio_decoder.as_mut().map(|(d, buffer)| {
let decoded_frames = match d.decode_float(&frame.data, buffer, false) { if let Ok(n) = d.decode_float(&frame.data, buffer, false) {
Ok(decoded_frames) => decoded_frames, let channels = self.channels;
Err(error) => { let n = n * (channels as usize);
log::warn!("Failed to decode audio frame: {error:?}"); #[cfg(not(target_os = "linux"))]
return; {
} let sample_rate0 = self.sample_rate.0;
}; let sample_rate = self.sample_rate.1;
let channels = self.channels; let mut buffer = buffer[0..n].to_owned();
let n = decoded_frames * channels as usize; if sample_rate != sample_rate0 {
#[cfg(not(target_os = "linux"))] buffer = crate::audio_resample(
{ &buffer[0..n],
let config = DecodedAudioConfig { sample_rate0,
sample_rate: self.sample_rate.1, sample_rate,
input_channels: self.channels, 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;
} }
}; if self.channels != self.device_channel {
self.audio_buffer.append_pcm(&buffer); buffer = crate::audio_rechannel(
} buffer,
#[cfg(target_os = "linux")] sample_rate,
{ sample_rate,
let data_u8 = self.channels,
unsafe { std::slice::from_raw_parts::<u8>(buffer.as_ptr() as _, n * 4) }; self.device_channel,
self.simple.as_mut().map(|x| x.write(data_u8)); );
}
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));
}
} }
}); });
} }
@@ -2498,28 +2350,63 @@ impl AudioHandler {
self.audio_buffer self.audio_buffer
.resize(config.sample_rate.0 as _, config.channels as _); .resize(config.sample_rate.0 as _, config.channels as _);
let audio_buffer = self.audio_buffer.0.clone(); let audio_buffer = self.audio_buffer.0.clone();
let discontinuity_generation = self.audio_buffer.3.clone(); let ready = self.ready.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 timeout = None;
let stream = device.build_output_stream( let stream = device.build_output_stream(
config, config,
move |data: &mut [T], _: &cpal::OutputCallbackInfo| { move |data: &mut [T], info: &cpal::OutputCallbackInfo| {
playback_writer.write_output(data); 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.),
};
}
}, },
err_fn, err_fn,
timeout, timeout,
)?; )?;
stream.play()?; stream.play()?;
self.audio_stream = Some(Box::new(stream)); self.audio_stream = Some(Box::new(stream));
self.playback_status = playback_status;
Ok(()) Ok(())
} }
} }
@@ -2539,27 +2426,6 @@ mod audio_format_tests {
assert!(!is_supported_audio_channel_count(0)); assert!(!is_supported_audio_channel_count(0));
assert!(!is_supported_audio_channel_count(u32::MAX)); 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`]. /// Video handler for the [`Client`].

View File

@@ -1,218 +0,0 @@
use hbb_common::{log, thiserror};
use ringbuf::{ring_buffer::RbBase, Rb};
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
TryLockError,
};
pub(super) const UNDERRUN_DECLICK_MS: usize = 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>,
}
#[derive(Default)]
pub(super) struct AudioPlaybackStatus {
pub(super) ready: AtomicBool,
contentions: AtomicUsize,
buffer_poisoned: AtomicBool,
}
impl AudioPlaybackStatus {
pub(super) fn report_errors(&self) {
let contentions = self.contentions.swap(0, Ordering::Relaxed);
if contentions != 0 {
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;

View File

@@ -1,218 +0,0 @@
use super::{AudioPlaybackConfig, AudioPlaybackError, AudioPlaybackRecovery, 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));
}

View File

@@ -1,113 +0,0 @@
use super::{create_audio_resampler, AudioDecoder, AudioFrame, AudioHandler, Stereo};
use cpal::traits::StreamTrait;
use hbb_common::anyhow::anyhow;
use magnum_opus::{Application::LowDelay, Encoder};
use ringbuf::{ring_buffer::RbBase, 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;
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()
}
}
#[test]
fn failed_format_change_discards_old_playback_state() {
let (mut handler, dropped) = active_handler(INPUT_RATE);
handler
.audio_buffer
.0
.lock()
.unwrap()
.push_slice(&[SAMPLE_VALUE; CHANNELS as usize]);
handler.audio_decoder = Some(decoder(OUTPUT_RATE));
handler.sample_rate = (OUTPUT_RATE, OUTPUT_RATE);
handler.handle_audio_start_result(
Err(anyhow!("Injected output stream startup failure")),
false,
);
assert!(dropped.load(Ordering::SeqCst));
assert!(handler.audio_stream.is_none());
assert!(handler.audio_resampler.is_none());
assert!(handler.audio_decoder.is_none());
assert!(!handler.playback_status.ready.load(Ordering::Acquire));
handler.handle_frame(audio_frame());
assert_eq!(handler.audio_buffer.0.lock().unwrap().occupied_len(), 0);
}
#[test]
fn successful_start_or_compatible_failure_preserves_audio_packet_duration() {
for result in [
Ok(()),
Err(anyhow!("Injected compatible stream replacement failure")),
] {
let (mut handler, dropped) = active_handler(OUTPUT_RATE);
handler.handle_audio_start_result(result, true);
handler.handle_frame(audio_frame());
assert!(!dropped.load(Ordering::SeqCst));
assert_eq!(
handler.audio_buffer.0.lock().unwrap().occupied_len(),
OUTPUT_RATE as usize / PACKETS_PER_SECOND * CHANNELS as usize
);
}
}

View File

@@ -15,16 +15,6 @@ use crate::{
// Restart msgbox text is kept as a legacy UI fallback; Flutter handles the type as a control event. // Restart msgbox text is kept as a legacy UI fallback; Flutter handles the type as a control event.
const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5); const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5);
const KCP_CLOSE_REASON_FLUSH_DELAY: Duration = Duration::from_millis(30); const KCP_CLOSE_REASON_FLUSH_DELAY: Duration = Duration::from_millis(30);
// Deadline for the parting close-reason send once the peer is presumed gone; KCP waits for send
// capacity with no deadline of its own.
const KCP_CLOSE_REASON_GONE_DEADLINE: Duration = Duration::from_millis(500);
// Grace after ICE reports Disconnected, which it does ~5s after it stops hearing from the peer,
// for ~8s in total. Disconnected is transient by design, so this waits out a Wi-Fi roam or a
// sleep/wake rather than acting on the first hint.
const WEBRTC_SUSPECT_GRACE: Duration = Duration::from_secs(3);
// KCP gets no such hint, only how long since a packet arrived; its endpoint pings an idle peer
// about every 2s, so this is several missed pings, and matches the 8s WebRTC arrives at.
const KCP_PEER_SILENCE_LIMIT: Duration = Duration::from_secs(8);
#[cfg(feature = "unix-file-copy-paste")] #[cfg(feature = "unix-file-copy-paste")]
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip}; use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
use base::{ use base::{
@@ -257,9 +247,6 @@ impl<T: InvokeUiSession> Remote<T> {
let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await; let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await;
let mut last_recv_time = Instant::now(); let mut last_recv_time = Instant::now();
let mut webrtc_suspect_since: Option<Instant> = None;
let mut last_rx_progress = peer.rx_progress();
let mut peer_gone = false;
loop { loop {
tokio::select! { tokio::select! {
@@ -326,37 +313,6 @@ impl<T: InvokeUiSession> Remote<T> {
self.handler.msgbox("restarting-show", "Restarting remote device", "Connection in progress. Please wait.", ""); self.handler.msgbox("restarting-show", "Restarting remote device", "Connection in progress. Please wait.", "");
break; break;
} }
let rx_progress = peer.rx_progress();
// `None` for transports that report none, and it never changes for a
// given one, so they are inert here.
let progressed = rx_progress != last_rx_progress;
last_rx_progress = rx_progress;
if peer.webrtc_disconnected() && !progressed {
webrtc_suspect_since.get_or_insert_with(Instant::now);
} else {
webrtc_suspect_since = None;
}
// Neither limit is a hard upper bound. A send is awaited inline in
// this loop, so one in progress delays this tick - bounded on WebRTC
// by the timeout the stream was built with, not bounded at all on
// KCP. The 30s watchdog above shares the loop and the same delay.
peer_gone = webrtc_suspect_since
.map_or(false, |since| since.elapsed() >= WEBRTC_SUSPECT_GRACE)
|| kcp
.as_ref()
.and_then(|k| k.peer_silent_for())
.map_or(false, |silent| silent >= KCP_PEER_SILENCE_LIMIT);
if peer_gone {
log::info!("Peer stopped answering, reconnecting");
#[cfg(feature = "flutter")]
self.handler.msgbox("restarting-show", "Connecting...", "Connection in progress. Please wait.", "");
// Sciter knows no `restarting-show` and would show a dialog that
// waits for a click, where the timeout this arrives ahead of is
// retryable and reconnects on its own. Keep that message for it.
#[cfg(not(feature = "flutter"))]
self.handler.msgbox("error", "Connection Error", "Timeout", "");
break;
}
let elapsed = fps_instant.elapsed().as_millis(); let elapsed = fps_instant.elapsed().as_millis();
if elapsed < 1000 { if elapsed < 1000 {
continue; continue;
@@ -402,11 +358,6 @@ impl<T: InvokeUiSession> Remote<T> {
s.send(()).ok(); s.send(()).ok();
} }
if kcp.is_some() { if kcp.is_some() {
// Attempted rather than skipped even here: if the loss was one-way the peer
// does get it, and drops its side instead of waiting out its own timeout.
if peer_gone {
peer.set_send_timeout(KCP_CLOSE_REASON_GONE_DEADLINE.as_millis() as u64);
}
// Send the close reason if it hasn't been sent yet, as KCP cannot detect the socket close event. // Send the close reason if it hasn't been sent yet, as KCP cannot detect the socket close event.
self.send_close_reason(&mut peer, "kcp").await; self.send_close_reason(&mut peer, "kcp").await;
// KCP does not send messages immediately, so wait to ensure the last message is sent. // KCP does not send messages immediately, so wait to ensure the last message is sent.

View File

@@ -59,14 +59,11 @@ impl Screenshot {
} }
fn handle_screenshot(&mut self, action: String) -> String { fn handle_screenshot(&mut self, action: String) -> String {
let Some(data) = self.data.as_ref().cloned() else { let Some(data) = self.data.take() else {
return "No cached screenshot".to_owned(); return "No cached screenshot".to_owned();
}; };
match Self::handle_screenshot_(data, action) { match Self::handle_screenshot_(data, action) {
Ok(()) => { Ok(()) => "".to_owned(),
self.data = None;
"".to_owned()
}
Err(e) => e.to_string(), Err(e) => e.to_string(),
} }
} }
@@ -101,37 +98,3 @@ pub fn set_screenshot(data: bytes::Bytes) {
pub fn handle_screenshot(action: String) -> String { pub fn handle_screenshot(action: String) -> String {
SCREENSHOT.lock().unwrap().handle_screenshot(action) SCREENSHOT.lock().unwrap().handle_screenshot(action)
} }
#[cfg(test)]
mod tests {
use super::Screenshot;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn preserves_cached_screenshot_when_save_fails() {
let data = bytes::Bytes::from_static(b"screenshot data");
let mut screenshot = Screenshot {
data: Some(data.clone()),
};
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let missing_parent = std::env::temp_dir()
.join(format!("rustdesk-screenshot-missing-parent-{unique}"))
.join("screenshot.png");
let valid_path = std::env::temp_dir().join(format!("rustdesk-screenshot-{unique}.png"));
let error = screenshot.handle_screenshot(format!("0:{}", missing_parent.display()));
assert!(!error.is_empty());
assert_eq!(screenshot.data.as_deref(), Some(data.as_ref()));
assert_eq!(
screenshot.handle_screenshot(format!("0:{}", valid_path.display())),
""
);
assert!(screenshot.data.is_none());
assert_eq!(std::fs::read(&valid_path).unwrap(), data.as_ref());
std::fs::remove_file(valid_path).unwrap();
}
}

View File

@@ -408,11 +408,6 @@ 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")] #[cfg(feature = "use_dasp")]
pub fn audio_resample( pub fn audio_resample(
data: &[f32], data: &[f32],
@@ -449,7 +444,7 @@ pub fn audio_resample(
} }
} }
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))] #[cfg(feature = "use_samplerate")]
pub fn audio_resample( pub fn audio_resample(
data: &[f32], data: &[f32],
sample_rate0: u32, sample_rate0: u32,

View File

@@ -8,15 +8,14 @@ use hbb_common::{
tokio_util, ResultType, Stream, tokio_util, ResultType, Stream,
}; };
use kcp_sys::{ use kcp_sys::{
endpoint::{ConnId, KcpEndpoint}, endpoint::KcpEndpoint,
packet_def::{KcpPacket, KcpPacketHeader}, packet_def::{KcpPacket, KcpPacketHeader},
stream, stream,
}; };
use std::{net::SocketAddr, sync::Arc}; use std::{net::SocketAddr, sync::Arc};
pub struct KcpStream { pub struct KcpStream {
endpoint: KcpEndpoint, _endpoint: KcpEndpoint,
conn_id: ConnId,
stop_sender: Option<oneshot::Sender<()>>, stop_sender: Option<oneshot::Sender<()>>,
} }
@@ -42,14 +41,6 @@ impl KcpStream {
} }
} }
/// How long since a valid packet was last received from the peer, or `None` once the
/// connection is gone. Answered by the KCP endpoint's own tasks, not by the session's read
/// loop, so it stays meaningful while that loop is busy sending a large message; and the
/// endpoint pings an idle peer often enough that silence here means the peer, not quiet.
pub fn peer_silent_for(&self) -> Option<std::time::Duration> {
self.endpoint.peer_silent_for(&self.conn_id)
}
fn create_framed(stream: stream::KcpStream, local_addr: Option<SocketAddr>) -> Stream { fn create_framed(stream: stream::KcpStream, local_addr: Option<SocketAddr>) -> Stream {
Stream::Tcp(FramedStream( Stream::Tcp(FramedStream(
tokio_util::codec::Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), tokio_util::codec::Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
@@ -86,8 +77,7 @@ impl KcpStream {
if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) { if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) {
Ok(( Ok((
Self { Self {
endpoint, _endpoint: endpoint,
conn_id,
stop_sender: Some(stop_sender), stop_sender: Some(stop_sender),
}, },
Self::create_framed(stream, udp_socket.local_addr().ok()), Self::create_framed(stream, udp_socket.local_addr().ok()),
@@ -118,8 +108,7 @@ impl KcpStream {
if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) { if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) {
Ok(( Ok((
Self { Self {
endpoint, _endpoint: endpoint,
conn_id,
stop_sender: Some(stop_sender), stop_sender: Some(stop_sender),
}, },
Self::create_framed(stream, udp_socket.local_addr().ok()), Self::create_framed(stream, udp_socket.local_addr().ok()),

View File

@@ -2,7 +2,6 @@ use hbb_common::regex::Regex;
use std::ops::Deref; use std::ops::Deref;
mod ar; mod ar;
mod az;
mod be; mod be;
mod bg; mod bg;
mod ca; mod ca;
@@ -106,7 +105,6 @@ pub const LANGS: &[(&str, &str)] = &[
("ml", "മലയാളം"), ("ml", "മലയാളം"),
("hi", "हिंदी"), ("hi", "हिंदी"),
("gu", "ગુજરાતી"), ("gu", "ગુજરાતી"),
("az", "Azərbaycan dili"),
]; ];
pub(crate) fn cjk_ui_unavailable() -> bool { pub(crate) fn cjk_ui_unavailable() -> bool {
@@ -222,7 +220,6 @@ pub fn translate_locale(name: String, locale: &str) -> String {
"hi" => hi::T.deref(), "hi" => hi::T.deref(),
"gu" => gu::T.deref(), "gu" => gu::T.deref(),
"gl" => gl::T.deref(), "gl" => gl::T.deref(),
"az" => az::T.deref(),
_ => en::T.deref(), _ => en::T.deref(),
}; };
let (name, placeholder_value) = extract_placeholder(&name); let (name, placeholder_value) = extract_placeholder(&name);

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "تمكين اتصال نظير إلى نظير عبر WebRTC"), ("Enable WebRTC P2P connection", "تمكين اتصال نظير إلى نظير عبر WebRTC"),
("Enable TCP hole punching", "تمكين تقنية حفر الثغرات عبر TCP"), ("Enable TCP hole punching", "تمكين تقنية حفر الثغرات عبر TCP"),
("The screen sharing request was declined on the remote device", "تم رفض طلب مشاركة الشاشة على الجهاز البعيد"), ("The screen sharing request was declined on the remote device", "تم رفض طلب مشاركة الشاشة على الجهاز البعيد"),
("The screen sharing request timed out on the remote device", "انتهت مهلة طلب مشاركة الشاشة على الجهاز البعيد"), ("No one responded to the screen sharing request 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 يمكنه استخدامها"), ("The XDG Desktop Portal ended the screen sharing request ({})", "أنهى XDG Desktop Portal طلب مشاركة الشاشة ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "بوابة سطح المكتب على الجهاز البعيد تفتقر إلى إمكانية لازمة لمشاركة الشاشة أو التحكم عن بُعد، قد لا تكون واجهتها الخلفية مثبتة"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "لم يُرجع XDG Desktop Portal أي شاشة لالتقاطها، قد تكون مكتبة PipeWire قديمة جدًا"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "تمت الموافقة على مشاركة الشاشة على الجهاز البعيد، لكن تعذّر فتح اتصال PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "مكوّن GStreamer الإضافي اللازم لالتقاط الشاشة مفقود ({})"),
("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 ثانية."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -1,776 +0,0 @@
lazy_static::lazy_static! {
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
("Status", "Vəziyyət"),
("Your Desktop", "Masaüstünüz"),
("desk_tip", "Masaüstünüzə bu ID və parol ilə giriş etmək olar."),
("Password", "Parol"),
("Ready", "Hazır"),
("Established", "Quruldu"),
("connecting_status", "RustDesk şəbəkəsinə qoşulur..."),
("Enable service", "Xidməti aktivləşdir"),
("Start service", "Xidməti başlat"),
("Service is running", "Xidmət işləyir"),
("Service is not running", "Xidmət işləmir"),
("not_ready_status", "Hazır deyil. Əlaqənizi yoxlayın"),
("Control Remote Desktop", "Uzaq masaüstünü idarə et"),
("Transfer file", "Fayl ötür"),
("Connect", "Qoşul"),
("Recent sessions", "Son sessiyalar"),
("Address book", "Ünvan kitabı"),
("Confirmation", "Təsdiq"),
("TCP tunneling", "TCP tunelləmə"),
("Remove", "Çıxar"),
("Refresh random password", "Təsadüfi parolu yenilə"),
("Set your own password", "Öz parolunuzu təyin edin"),
("Enable keyboard/mouse", "Klaviaturanı/siçanı aktivləşdir"),
("Enable clipboard", "Mübadilə buferini aktivləşdir"),
("Enable file transfer", "Fayl ötürülməsini aktivləşdir"),
("Enable TCP tunneling", "TCP tunelləməni aktivləşdir"),
("IP Whitelisting", "IP ağ siyahısı"),
("ID/Relay Server", "ID/Relay serveri"),
("Import server config", "Server konfiqurasiyasını idxal et"),
("Export Server Config", "Server konfiqurasiyasını ixrac et"),
("Import server configuration successfully", "Server konfiqurasiyası uğurla idxal edildi"),
("Export server configuration successfully", "Server konfiqurasiyası uğurla ixrac edildi"),
("Invalid server configuration", "Yanlış server konfiqurasiyası"),
("Clipboard is empty", "Mübadilə buferi boşdur"),
("Stop service", "Xidməti dayandır"),
("Change ID", "ID-ni dəyiş"),
("Your new ID", "Yeni ID-niz"),
("length %min% to %max%", "uzunluq %min% ilə %max% arasında"),
("starts with a letter", "hərflə başlayır"),
("allowed characters", "icazə verilən simvollar"),
("id_change_tip", "Yalnız a-z, A-Z, 0-9, - (defis) və _ (alt xətt) simvollarına icazə verilir. İlk hərf a-z, A-Z olmalıdır. Uzunluq 6 ilə 16 arasında."),
("Website", "Veb sayt"),
("About", "Haqqında"),
("Slogan_tip", "Bu qarışıq dünyada ürəklə hazırlanıb!"),
("Privacy Statement", "Məxfilik bəyanatı"),
("Mute", "Səssiz"),
("Build Date", "Yığılma tarixi"),
("Version", "Versiya"),
("Home", "Əsas səhifə"),
("Audio Input", "Audio girişi"),
("Enhancements", "Təkmilləşdirmələr"),
("Hardware Codec", "Aparat kodeki"),
("Adaptive bitrate", "Adaptiv bitreyt"),
("ID Server", "ID serveri"),
("Relay Server", "Relay serveri"),
("API Server", "API serveri"),
("invalid_http", "http:// və ya https:// ilə başlamalıdır"),
("Invalid IP", "Yanlış IP"),
("Invalid format", "Yanlış format"),
("server_not_support", "Server hələ dəstəkləmir"),
("Not available", "Əlçatan deyil"),
("Too frequent", "Çox tez-tez"),
("Cancel", "Ləğv et"),
("Skip", "Keç"),
("Close", "Bağla"),
("Retry", "Yenidən cəhd et"),
("OK", "OK"),
("Password Required", "Parol tələb olunur"),
("Please enter your password", "Parolunuzu daxil edin"),
("Remember password", "Parolu yadda saxla"),
("Wrong Password", "Yanlış parol"),
("Do you want to enter again?", "Yenidən daxil etmək istəyirsiniz?"),
("Connection Error", "Əlaqə xətası"),
("Error", "Xəta"),
("Reset by the peer", "Qarşı tərəf əlaqəni sıfırladı"),
("Connecting...", "Qoşulur..."),
("Connection in progress. Please wait.", "Əlaqə qurulur. Gözləyin."),
("Please try 1 minute later", "1 dəqiqə sonra cəhd edin"),
("Login Error", "Giriş xətası"),
("Successful", "Uğurlu"),
("Connected, waiting for image...", "Qoşuldu, şəkil gözlənilir..."),
("Name", "Ad"),
("Type", "Növ"),
("Modified", "Dəyişdirilib"),
("Size", "Ölçü"),
("Show Hidden Files", "Gizli faylları göstər"),
("Receive", "Qəbul et"),
("Send", "Göndər"),
("Refresh File", "Faylı yenilə"),
("Local", "Lokal"),
("Remote", "Uzaq"),
("Remote Computer", "Uzaq kompüter"),
("Local Computer", "Lokal kompüter"),
("Confirm Delete", "Silinməni təsdiqlə"),
("Delete", "Sil"),
("Properties", "Xüsusiyyətlər"),
("Multi Select", "Çoxlu seçim"),
("Select All", "Hamısını seç"),
("Unselect All", "Seçimi ləğv et"),
("Empty Directory", "Boş qovluq"),
("Not an empty directory", "Qovluq boş deyil"),
("Are you sure you want to delete this file?", "Bu faylı silmək istədiyinizə əminsiniz?"),
("Are you sure you want to delete this empty directory?", "Bu boş qovluğu silmək istədiyinizə əminsiniz?"),
("Are you sure you want to delete the file of this directory?", "Bu qovluğun faylını silmək istədiyinizə əminsiniz?"),
("Do this for all conflicts", "Bunu bütün ziddiyyətlər üçün et"),
("This is irreversible!", "Bu geri qaytarıla bilməz!"),
("Deleting", "Silinir"),
("files", "fayl"),
("Waiting", "Gözlənilir"),
("Finished", "Bitdi"),
("Speed", "Sürət"),
("Custom Image Quality", "Fərdi şəkil keyfiyyəti"),
("Privacy mode", "Məxfilik rejimi"),
("Block user input", "İstifadəçi girişini blokla"),
("Unblock user input", "İstifadəçi girişinin blokunu aç"),
("Adjust Window", "Pəncərəni uyğunlaşdır"),
("Original", "Orijinal"),
("Shrink", "Kiçilt"),
("Stretch", "Uzat"),
("Scrollbar", "Sürüşdürmə zolağı"),
("ScrollAuto", "Avtomatik sürüşdürmə"),
("Good image quality", "Yaxşı şəkil keyfiyyəti"),
("Balanced", "Balanslı"),
("Optimize reaction time", "Reaksiya vaxtını optimallaşdır"),
("Custom", "Fərdi"),
("Show remote cursor", "Uzaq kursoru göstər"),
("Show quality monitor", "Keyfiyyət monitorunu göstər"),
("Disable clipboard", "Mübadilə buferini söndür"),
("Lock after session end", "Sessiya bitdikdən sonra kilidlə"),
("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del göndər"),
("Insert Lock", "Kilid göndər"),
("Refresh", "Yenilə"),
("ID does not exist", "ID mövcud deyil"),
("Failed to connect to rendezvous server", "Rendezvous serverinə qoşulmaq alınmadı"),
("Please try later", "Sonra cəhd edin"),
("Remote desktop is offline", "Uzaq masaüstü oflayndır"),
("Key mismatch", "Açar uyğun gəlmir"),
("Timeout", "Vaxt bitdi"),
("Failed to connect to relay server", "Relay serverinə qoşulmaq alınmadı"),
("Failed to connect via rendezvous server", "Rendezvous serveri vasitəsilə qoşulmaq alınmadı"),
("Failed to connect via relay server", "Relay serveri vasitəsilə qoşulmaq alınmadı"),
("Failed to make direct connection to remote desktop", "Uzaq masaüstünə birbaşa əlaqə qurmaq alınmadı"),
("Set Password", "Parolu təyin et"),
("OS Password", "Əməliyyat sistemi parolu"),
("install_tip", "UAC səbəbindən RustDesk bəzi hallarda uzaq tərəf kimi düzgün işləyə bilmir. UAC-dan yayınmaq üçün aşağıdakı düyməyə basaraq RustDesk-i sistemə quraşdırın."),
("Click to upgrade", "Yeniləmək üçün klikləyin"),
("Configure", "Konfiqurasiya et"),
("config_acc", "Masaüstünüzü uzaqdan idarə etmək üçün RustDesk-ə \"Əlçatanlıq\" icazələrini verməlisiniz."),
("config_screen", "Masaüstünüzə uzaqdan giriş üçün RustDesk-ə \"Ekran Yazısı\" icazələrini verməlisiniz."),
("Installing ...", "Quraşdırılır ..."),
("Install", "Quraşdır"),
("Installation", "Quraşdırma"),
("Installation Path", "Quraşdırma yolu"),
("Create start menu shortcuts", "Başlat menyusu qısayolları yarat"),
("Create desktop icon", "Masaüstü ikonu yarat"),
("agreement_tip", "Quraşdırmanı başlatmaqla lisenziya müqaviləsini qəbul edirsiniz."),
("Accept and Install", "Qəbul et və quraşdır"),
("End-user license agreement", "Son istifadəçi lisenziya müqaviləsi"),
("Generating ...", "Yaradılır ..."),
("Your installation is lower version.", "Quraşdırdığınız versiya köhnədir."),
("not_close_tcp_tip", "Tuneldən istifadə edərkən bu pəncərəni bağlamayın"),
("Listening ...", "Dinlənilir ..."),
("Remote Host", "Uzaq host"),
("Remote Port", "Uzaq port"),
("Action", "Əməliyyat"),
("Add", "Əlavə et"),
("Local Port", "Lokal port"),
("Local Address", "Lokal ünvan"),
("Change Local Port", "Lokal portu dəyiş"),
("setup_server_tip", "Daha sürətli əlaqə üçün öz serverinizi qurun"),
("Too short, at least 6 characters.", "Çox qısadır, ən azı 6 simvol olmalıdır."),
("The confirmation is not identical.", "Təsdiq eyni deyil."),
("Permissions", "İcazələr"),
("Accept", "Qəbul et"),
("Dismiss", "İmtina et"),
("Disconnect", "Əlaqəni kəs"),
("Enable file copy and paste", "Fayl kopyalama və yapışdırmanı aktivləşdir"),
("Connected", "Qoşuldu"),
("Direct and encrypted connection", "Birbaşa və şifrələnmiş əlaqə"),
("Relayed and encrypted connection", "Relay üzərindən şifrələnmiş əlaqə"),
("Direct and unencrypted connection", "Birbaşa və şifrələnməmiş əlaqə"),
("Relayed and unencrypted connection", "Relay üzərindən şifrələnməmiş əlaqə"),
("Enter Remote ID", "Uzaq ID-ni daxil edin"),
("Enter your password", "Parolunuzu daxil edin"),
("Logging in...", "Giriş edilir..."),
("Enable RDP session sharing", "RDP sessiya paylaşımını aktivləşdir"),
("Auto Login", "Avtomatik giriş (yalnız \"Sessiya bitdikdən sonra kilidlə\" seçilibsə işləyir)"),
("Enable direct IP access", "Birbaşa IP girişini aktivləşdir"),
("Rename", "Adını dəyiş"),
("Space", "Boşluq"),
("Create desktop shortcut", "Masaüstü qısayolu yarat"),
("Change Path", "Yolu dəyiş"),
("Create Folder", "Qovluq yarat"),
("Please enter the folder name", "Qovluğun adını daxil edin"),
("Fix it", "Düzəlt"),
("Warning", "Xəbərdarlıq"),
("Login screen using Wayland is not supported", "Wayland ilə giriş ekranı dəstəklənmir"),
("Reboot required", "Yenidən başlatma tələb olunur"),
("Unsupported display server", "Dəstəklənməyən displey serveri"),
("x11 expected", "x11 gözlənilir"),
("Port", "Port"),
("Settings", "Parametrlər"),
("Username", "İstifadəçi adı"),
("Invalid port", "Yanlış port"),
("Closed manually by the peer", "Qarşı tərəf əl ilə bağladı"),
("Enable remote configuration modification", "Uzaqdan konfiqurasiya dəyişikliyini aktivləşdir"),
("Run without install", "Quraşdırmadan işə sal"),
("Connect via relay", "Relay vasitəsilə qoşul"),
("Always connect via relay", "Həmişə relay vasitəsilə qoşul"),
("whitelist_tip", "Yalnız ağ siyahıdakı IP mənə giriş edə bilər"),
("Login", "Giriş"),
("Verify", "Doğrula"),
("Remember me", "Məni xatırla"),
("Trust this device", "Bu cihaza etibar et"),
("Verification code", "Doğrulama kodu"),
("verification_tip", "Qeydiyyatdan keçmiş e-poçt ünvanına doğrulama kodu göndərildi, girişi davam etdirmək üçün kodu daxil edin."),
("Logout", "Çıxış"),
("Tags", "Etiketlər"),
("Search ID", "ID axtar"),
("whitelist_sep", "Vergül, nöqtəli vergül, boşluq və ya yeni sətirlə ayrılır"),
("Add ID", "ID əlavə et"),
("Add Tag", "Etiket əlavə et"),
("Unselect all tags", "Bütün etiketlərin seçimini ləğv et"),
("Network error", "Şəbəkə xətası"),
("Username missed", "İstifadəçi adı yazılmayıb"),
("Password missed", "Parol yazılmayıb"),
("Wrong credentials", "Yanlış istifadəçi adı və ya parol"),
("The verification code is incorrect or has expired", "Doğrulama kodu yanlışdır və ya vaxtı bitib"),
("Edit Tag", "Etiketi redaktə et"),
("Forget Password", "Parolu unut"),
("Favorites", "Seçilmişlər"),
("Add to Favorites", "Seçilmişlərə əlavə et"),
("Remove from Favorites", "Seçilmişlərdən çıxar"),
("Empty", "Boş"),
("Invalid folder name", "Yanlış qovluq adı"),
("Socks5 Proxy", "Socks5 proksi"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) proksi"),
("Discovered", "Aşkarlandı"),
("install_daemon_tip", "Sistem açılışında başlaması üçün sistem xidmətini quraşdırmalısınız."),
("Remote ID", "Uzaq ID"),
("Paste", "Yapışdır"),
("Paste here?", "Buraya yapışdırılsın?"),
("Are you sure to close the connection?", "Əlaqəni bağlamaq istədiyinizə əminsiniz?"),
("Download new version", "Yeni versiyanı endir"),
("Touch mode", "Toxunuş rejimi"),
("Mouse mode", "Siçan rejimi"),
("One-Finger Tap", "Bir barmaqla toxunuş"),
("Left Mouse", "Sol siçan düyməsi"),
("One-Long Tap", "Bir barmaqla uzun toxunuş"),
("Two-Finger Tap", "İki barmaqla toxunuş"),
("Right Mouse", "Sağ siçan düyməsi"),
("One-Finger Move", "Bir barmaqla hərəkət"),
("Double Tap & Move", "İkiqat toxunuş və hərəkət"),
("Mouse Drag", "Siçanla sürükləmə"),
("Three-Finger vertically", "Üç barmaqla şaquli"),
("Mouse Wheel", "Siçan çarxı"),
("Two-Finger Move", "İki barmaqla hərəkət"),
("Canvas Move", "Kətanın hərəkəti"),
("Pinch to Zoom", "Barmaqlarla yaxınlaşdırma"),
("Canvas Zoom", "Kətanın miqyası"),
("Reset canvas", "Kətanı sıfırla"),
("No permission of file transfer", "Fayl ötürülməsi üçün icazə yoxdur"),
("Note", "Qeyd"),
("Connection", "Əlaqə"),
("Share screen", "Ekranı paylaş"),
("Chat", "Söhbət"),
("Total", "Ümumi"),
("items", "element"),
("Selected", "Seçilib"),
("Screen Capture", "Ekran çəkilişi"),
("Input Control", "Giriş idarəsi"),
("Audio Capture", "Audio çəkilişi"),
("Do you accept?", "Qəbul edirsiniz?"),
("Open System Setting", "Sistem parametrini aç"),
("How to get Android input permission?", "Android giriş icazəsi necə alınır?"),
("android_input_permission_tip1", "Uzaq cihazın siçan və ya toxunuşla Android cihazınızı idarə etməsi üçün RustDesk-ə \"Əlçatanlıq\" xidmətindən istifadə icazəsi verməlisiniz."),
("android_input_permission_tip2", "Növbəti sistem parametrləri səhifəsinə keçin, [Quraşdırılmış xidmətlər] bölməsini tapıb açın və [RustDesk Input] xidmətini işə salın."),
("android_new_connection_tip", "Cari cihazınızı idarə etmək istəyən yeni idarəetmə sorğusu alındı."),
("android_service_will_start_tip", "\"Ekran çəkilişi\"ni işə salmaq xidməti avtomatik başladacaq və digər cihazlara cihazınıza əlaqə sorğusu göndərməyə imkan verəcək."),
("android_stop_service_tip", "Xidməti bağlamaq qurulmuş bütün əlaqələri avtomatik olaraq bağlayacaq."),
("android_version_audio_tip", "Cari Android versiyası audio çəkilişini dəstəkləmir, Android 10 və ya daha yuxarı versiyaya yüksəldin."),
("android_start_service_tip", "Ekran paylaşımı xidmətini başlatmaq üçün [Xidməti başlat] düyməsinə toxunun və ya [Ekran çəkilişi] icazəsini aktivləşdirin."),
("android_permission_may_not_change_tip", "Qurulmuş əlaqələrin icazələri yenidən qoşulana qədər dərhal dəyişməyə bilər."),
("Account", "Hesab"),
("Overwrite", "Üzərinə yaz"),
("This file exists, skip or overwrite this file?", "Bu fayl mövcuddur, keçilsin yoxsa üzərinə yazılsın?"),
("Quit", "Çıx"),
("Help", "Kömək"),
("Failed", "Alınmadı"),
("Succeeded", "Uğurlu oldu"),
("Someone turns on privacy mode, exit", "Kimsə məxfilik rejimini açdı, çıxılır"),
("Unsupported", "Dəstəklənmir"),
("Peer denied", "Qarşı tərəf imtina etdi"),
("Peer exit", "Qarşı tərəf çıxdı"),
("Failed to turn off", "Söndürmək alınmadı"),
("Turned off", "Söndürüldü"),
("Language", "Dil"),
("Keep RustDesk background service", "RustDesk fon xidmətini işlək saxla"),
("Ignore Battery Optimizations", "Batareya optimallaşdırmalarını nəzərə alma"),
("android_open_battery_optimizations_tip", "Bu funksiyanı söndürmək istəyirsinizsə, növbəti RustDesk tətbiq parametrləri səhifəsinə keçin, [Batareya] bölməsini tapıb açın və [Məhdudiyyətsiz] seçimini götürün"),
("Start on boot", "Sistem açılışında başlat"),
("Start the screen sharing service on boot, requires special permissions", "Ekran paylaşımı xidmətini sistem açılışında başlat, xüsusi icazələr tələb olunur"),
("Connection not allowed", "Əlaqəyə icazə verilmir"),
("Legacy mode", "Köhnə rejim"),
("Map mode", "Xəritə rejimi"),
("Translate mode", "Çevirmə rejimi"),
("Use permanent password", "Daimi paroldan istifadə et"),
("Use both passwords", "Hər iki paroldan istifadə et"),
("Set permanent password", "Daimi parolu təyin et"),
("Enable remote restart", "Uzaqdan yenidən başlatmanı aktivləşdir"),
("Restart remote device", "Uzaq cihazı yenidən başlat"),
("Are you sure you want to restart", "Yenidən başlatmaq istədiyinizə əminsiniz"),
("Restarting remote device", "Uzaq cihaz yenidən başladılır"),
("remote_restarting_tip", "Uzaq cihaz yenidən başladılır, bu mesaj qutusunu bağlayın və bir az sonra daimi parolla yenidən qoşulun"),
("Copied", "Kopyalandı"),
("Exit Fullscreen", "Tam ekrandan çıx"),
("Fullscreen", "Tam ekran"),
("Mobile Actions", "Mobil əməliyyatlar"),
("Select Monitor", "Monitoru seç"),
("Control Actions", "İdarəetmə əməliyyatları"),
("Display Settings", "Ekran parametrləri"),
("Ratio", "Nisbət"),
("Image Quality", "Şəkil keyfiyyəti"),
("Scroll Style", "Sürüşdürmə üslubu"),
("Show Toolbar", "Alətlər panelini göstər"),
("Hide Toolbar", "Alətlər panelini gizlət"),
("Direct Connection", "Birbaşa əlaqə"),
("Relay Connection", "Relay əlaqəsi"),
("Secure Connection", "Təhlükəsiz əlaqə"),
("Insecure Connection", "Təhlükəsiz olmayan əlaqə"),
("Scale original", "Orijinal miqyas"),
("Scale adaptive", "Uyğunlaşan miqyas"),
("General", "Ümumi"),
("Security", "Təhlükəsizlik"),
("Theme", "Tema"),
("Dark Theme", "Tünd tema"),
("Light Theme", "ıq tema"),
("Dark", "Tünd"),
("Light", "ıq"),
("Follow System", "Sistemə uyğun"),
("Enable hardware codec", "Aparat kodekini aktivləşdir"),
("Unlock Security Settings", "Təhlükəsizlik parametrlərinin kilidini aç"),
("Enable audio", "Audionu aktivləşdir"),
("Unlock Network Settings", "Şəbəkə parametrlərinin kilidini aç"),
("Server", "Server"),
("Direct IP Access", "Birbaşa IP girişi"),
("Proxy", "Proksi"),
("Apply", "Tətbiq et"),
("Disconnect all devices?", "Bütün cihazlarla əlaqə kəsilsin?"),
("Clear", "Təmizlə"),
("Audio Input Device", "Audio giriş cihazı"),
("Use IP Whitelisting", "IP ağ siyahısından istifadə et"),
("Network", "Şəbəkə"),
("Pin Toolbar", "Alətlər panelini sancaqla"),
("Unpin Toolbar", "Alətlər panelinin sancağını çıxar"),
("Recording", "Yazma"),
("Directory", "Qovluq"),
("Automatically record incoming sessions", "Gələn sessiyaları avtomatik yaz"),
("Automatically record outgoing sessions", "Gedən sessiyaları avtomatik yaz"),
("Change", "Dəyiş"),
("Start session recording", "Sessiya yazısını başlat"),
("Stop session recording", "Sessiya yazısını dayandır"),
("Enable recording session", "Sessiya yazısını aktivləşdir"),
("Enable LAN discovery", "LAN aşkarlanmasını aktivləşdir"),
("Deny LAN discovery", "LAN aşkarlanmasına icazə vermə"),
("Write a message", "Mesaj yazın"),
("Prompt", "Sorğu"),
("Please wait for confirmation of UAC...", "UAC təsdiqini gözləyin..."),
("elevated_foreground_window_tip", "Uzaq masaüstünün cari pəncərəsi işləmək üçün daha yüksək səlahiyyət tələb edir, ona görə siçan və klaviaturadan müvəqqəti istifadə etmək mümkün deyil. Uzaq istifadəçidən cari pəncərəni kiçiltməsini xahiş edə və ya əlaqə idarəetmə pəncərəsindəki səlahiyyət yüksəltmə düyməsini basa bilərsiniz. Bu problemin qarşısını almaq üçün proqramı uzaq cihaza quraşdırmaq tövsiyə olunur."),
("Disconnected", "Əlaqə kəsildi"),
("Other", "Digər"),
("Confirm before closing multiple tabs", "Çoxlu tabı bağlamazdan əvvəl təsdiq soruş"),
("Keyboard Settings", "Klaviatura parametrləri"),
("Full Access", "Tam giriş"),
("Screen Share", "Ekran paylaşımı"),
("ubuntu-21-04-required", "Wayland Ubuntu 21.04 və ya daha yuxarı versiya tələb edir."),
("wayland-requires-higher-linux-version", "Wayland daha yuxarı Linux distributiv versiyası tələb edir. X11 masaüstünü sınayın və ya əməliyyat sisteminizi dəyişin."),
("xdp-portal-unavailable", "Wayland ekran çəkilişi alınmadı. XDG Desktop Portal çökmüş və ya əlçatmaz ola bilər. `systemctl --user restart xdg-desktop-portal` ilə yenidən başlatmağı sınayın."),
("JumpLink", "Bax"),
("Please Select the screen to be shared(Operate on the peer side).", "Paylaşılacaq ekranı seçin(Qarşı tərəfdə edilir)."),
("Show RustDesk", "RustDesk-i göstər"),
("This PC", "Bu kompüter"),
("or", "və ya"),
("Elevate", "Səlahiyyəti yüksəlt"),
("Zoom cursor", "Kursoru böyüt"),
("Accept sessions via password", "Sessiyaları parolla qəbul et"),
("Accept sessions via click", "Sessiyaları kliklə qəbul et"),
("Accept sessions via both", "Sessiyaları hər ikisi ilə qəbul et"),
("Please wait for the remote side to accept your session request...", "Uzaq tərəfin sessiya sorğunuzu qəbul etməsini gözləyin..."),
("One-time Password", "Birdəfəlik parol"),
("Use one-time password", "Birdəfəlik paroldan istifadə et"),
("One-time password length", "Birdəfəlik parolun uzunluğu"),
("Request access to your device", "Cihazınıza giriş sorğusu"),
("Hide connection management window", "Əlaqə idarəetmə pəncərəsini gizlət"),
("hide_cm_tip", "Gizlətməyə yalnız sessiyalar parolla qəbul edilirsə və daimi paroldan istifadə olunursa icazə verilir"),
("wayland_experiment_tip", "Wayland dəstəyi eksperimental mərhələdədir, nəzarətsiz giriş lazımdırsa X11 işlədin."),
("Right click to select tabs", "Tabları seçmək üçün sağ klikləyin"),
("Skipped", "Keçildi"),
("Add to address book", "Ünvan kitabına əlavə et"),
("Group", "Qrup"),
("Search", "Axtarış"),
("Closed manually by web console", "Veb konsoldan əl ilə bağlandı"),
("Local keyboard type", "Lokal klaviatura növü"),
("Select local keyboard type", "Lokal klaviatura növünü seçin"),
("software_render_tip", "Linux-da Nvidia video kartından istifadə edirsinizsə və qoşulduqdan dərhal sonra uzaq pəncərə bağlanırsa, açıq mənbəli Nouveau sürücüsünə keçmək və proqram təminatı ilə render seçmək kömək edə bilər. Proqramın yenidən başladılması tələb olunur."),
("Always use software rendering", "Həmişə proqram təminatı ilə render işlət"),
("config_input", "Uzaq masaüstünü klaviatura ilə idarə etmək üçün RustDesk-ə \"Giriş Monitorinqi\" icazələrini verməlisiniz."),
("config_microphone", "Uzaqdan danışmaq üçün RustDesk-ə \"Audio Yazısı\" icazələrini verməlisiniz."),
("request_elevation_tip", "Uzaq tərəfdə kimsə varsa, səlahiyyət yüksəltmə də tələb edə bilərsiniz."),
("Wait", "Gözlə"),
("Elevation Error", "Səlahiyyət yüksəltmə xətası"),
("Ask the remote user for authentication", "Uzaq istifadəçidən doğrulama istə"),
("Choose this if the remote account is administrator", "Uzaq hesab administratordursa bunu seçin"),
("Transmit the username and password of administrator", "Administratorun istifadəçi adını və parolunu ötür"),
("still_click_uac_tip", "Yenə də uzaq istifadəçinin işləyən RustDesk-in UAC pəncərəsində OK düyməsini basması tələb olunur."),
("Request Elevation", "Səlahiyyət yüksəltmə tələb et"),
("wait_accept_uac_tip", "Uzaq istifadəçinin UAC dialoqunu qəbul etməsini gözləyin."),
("Elevate successfully", "Səlahiyyət uğurla yüksəldildi"),
("uppercase", "böyük hərf"),
("lowercase", "kiçik hərf"),
("digit", "rəqəm"),
("special character", "xüsusi simvol"),
("length>=8", "uzunluq>=8"),
("Weak", "Zəif"),
("Medium", "Orta"),
("Strong", "Güclü"),
("Switch Sides", "Tərəfləri dəyiş"),
("Please confirm if you want to share your desktop?", "Masaüstünüzü paylaşmaq istədiyinizi təsdiqləyin?"),
("Display", "Ekran"),
("Default View Style", "Standart baxış üslubu"),
("Default Scroll Style", "Standart sürüşdürmə üslubu"),
("Default Image Quality", "Standart şəkil keyfiyyəti"),
("Default Codec", "Standart kodek"),
("Bitrate", "Bitreyt"),
("FPS", "FPS"),
("Auto", "Avtomatik"),
("Other Default Options", "Digər standart seçimlər"),
("Voice call", "Səsli zəng"),
("Text chat", "Mətn söhbəti"),
("Stop voice call", "Səsli zəngi dayandır"),
("relay_hint_tip", "Birbaşa qoşulmaq mümkün olmaya bilər; relay vasitəsilə qoşulmağı sınaya bilərsiniz. Bundan başqa, ilk cəhddə relay işlətmək istəyirsinizsə, ID-yə \"/r\" şəkilçisi əlavə edin və ya son sessiyalar kartında varsa \"Həmişə relay vasitəsilə qoşul\" seçimini işarələyin."),
("Reconnect", "Yenidən qoşul"),
("Codec", "Kodek"),
("Resolution", "Ayırdetmə"),
("No transfers in progress", "Davam edən ötürülmə yoxdur"),
("Set one-time password length", "Birdəfəlik parolun uzunluğunu təyin et"),
("RDP Settings", "RDP parametrləri"),
("Sort by", "Sıralama"),
("New Connection", "Yeni əlaqə"),
("Restore", "Bərpa et"),
("Minimize", "Kiçilt"),
("Maximize", "Böyüt"),
("Your Device", "Cihazınız"),
("empty_recent_tip", "Təəssüf, son sessiya yoxdur!\nYenisini planlaşdırmaq vaxtıdır."),
("empty_favorite_tip", "Hələ seçilmiş cihaz yoxdur?\nGəlin qoşulacaq birini tapıb seçilmişlərə əlavə edək!"),
("empty_lan_tip", "Görünür, hələ heç bir cihaz aşkarlanmayıb."),
("empty_address_book_tip", "Görünür, ünvan kitabınızda hazırda heç bir cihaz yoxdur."),
("Empty Username", "Boş istifadəçi adı"),
("Empty Password", "Boş parol"),
("Me", "Mən"),
("identical_file_tip", "Bu fayl qarşı tərəfdəki ilə eynidir."),
("show_monitors_tip", "Monitorları alətlər panelində göstər"),
("View Mode", "Baxış rejimi"),
("verify_rustdesk_password_tip", "RustDesk parolunu doğrula"),
("No need to elevate", "Səlahiyyəti yüksəltməyə ehtiyac yoxdur"),
("System Sound", "Sistem səsi"),
("Default", "Standart"),
("New RDP", "Yeni RDP"),
("Fingerprint", "Barmaq izi"),
("Copy Fingerprint", "Barmaq izini kopyala"),
("no fingerprints", "Barmaq izi yoxdur"),
("Update", "Yenilə"),
("resolution_original_tip", "Orijinal ayırdetmə"),
("resolution_fit_local_tip", "Lokal ayırdetməyə uyğunlaşdır"),
("resolution_custom_tip", "Fərdi ayırdetmə"),
("Collapse toolbar", "Alətlər panelini yığ"),
("Accept and Elevate", "Qəbul et və səlahiyyəti yüksəlt"),
("accept_and_elevate_btn_tooltip", "Əlaqəni qəbul et və UAC icazələrini yüksəlt."),
("clipboard_wait_response_timeout_tip", "Kopyalama cavabı gözlənilərkən vaxt bitdi."),
("Incoming connection", "Gələn əlaqə"),
("Outgoing connection", "Gedən əlaqə"),
("Exit", "Çıx"),
("Open", ""),
("logout_tip", "Çıxmaq istədiyinizə əminsiniz?"),
("Service", "Xidmət"),
("Start", "Başlat"),
("Stop", "Dayandır"),
("exceed_max_devices", "İdarə olunan cihazların maksimum sayına çatmısınız."),
("Sync with recent sessions", "Son sessiyalarla sinxronlaşdır"),
("Sort tags", "Etiketləri sırala"),
("Open connection in new tab", "Əlaqəni yeni tabda aç"),
("Move tab to new window", "Tabı yeni pəncərəyə köçür"),
("Can not be empty", "Boş ola bilməz"),
("Already exists", "Artıq mövcuddur"),
("Change Password", "Parolu dəyiş"),
("Refresh Password", "Parolu yenilə"),
("ID", "ID"),
("Grid View", "Tor görünüşü"),
("List View", "Siyahı görünüşü"),
("Select", "Seç"),
("Toggle Tags", "Etiketləri aç/bağla"),
("pull_ab_failed_tip", "Ünvan kitabını yeniləmək alınmadı"),
("push_ab_failed_tip", "Ünvan kitabını serverlə sinxronlaşdırmaq alınmadı"),
("synced_peer_readded_tip", "Son sessiyalarda olan cihazlar ünvan kitabına geri sinxronlaşdırılacaq."),
("Change Color", "Rəngi dəyiş"),
("Primary Color", "Əsas rəng"),
("HSV Color", "HSV rəngi"),
("Installation Successful!", "Quraşdırma uğurlu oldu!"),
("Installation failed!", "Quraşdırma alınmadı!"),
("Reverse mouse wheel", "Siçan çarxının istiqamətini tərsinə çevir"),
("{} sessions", "{} sessiya"),
("scam_title", "SİZİ ALDADA BİLƏRLƏR!"),
("scam_text1", "Tanımadığınız və ETİBAR ETMƏDİYİNİZ biri telefonda sizdən RustDesk işlətməyi və xidməti başlatmağı xahiş edirsə, davam etməyin və dərhal telefonu bağlayın."),
("scam_text2", "Böyük ehtimalla o, pulunuzu və ya digər şəxsi məlumatlarınızı oğurlamağa çalışan fırıldaqçıdır."),
("Don't show again", "Bir daha göstərmə"),
("I Agree", "Razıyam"),
("Decline", "Rədd et"),
("Timeout in minutes", "Dəqiqə ilə gözləmə müddəti"),
("auto_disconnect_option_tip", "İstifadəçi fəaliyyət göstərmədikdə gələn sessiyaları avtomatik bağla"),
("Connection failed due to inactivity", "Fəaliyyətsizliyə görə əlaqə avtomatik kəsildi"),
("Check for software update on startup", "Başlanğıcda proqram yeniləməsini yoxla"),
("upgrade_rustdesk_server_pro_to_{}_tip", "RustDesk Server Pro-nu {} və ya daha yeni versiyaya yüksəldin!"),
("pull_group_failed_tip", "Qrupu yeniləmək alınmadı"),
("Filter by intersection", "Kəsişməyə görə süz"),
("Remove wallpaper during incoming sessions", "Gələn sessiyalar zamanı divar kağızını götür"),
("Test", "Sına"),
("display_is_plugged_out_msg", "Ekran ayrıldı, birinci ekrana keçilir."),
("No displays", "Ekran yoxdur"),
("Open in new window", "Yeni pəncərədə aç"),
("Show displays as individual windows", "Ekranları ayrı pəncərələr kimi göstər"),
("Use all my displays for the remote session", "Uzaq sessiya üçün bütün ekranlarımı işlət"),
("selinux_tip", "Cihazınızda SELinux aktivdir, bu, RustDesk-in idarə olunan tərəf kimi düzgün işləməsinə mane ola bilər."),
("Change view", "Görünüşü dəyiş"),
("Big tiles", "Böyük xanalar"),
("Small tiles", "Kiçik xanalar"),
("List", "Siyahı"),
("Virtual display", "Virtual ekran"),
("Plug out all", "Hamısını ayır"),
("True color (4:4:4)", "Tam rəng (4:4:4)"),
("Enable blocking user input", "İstifadəçi girişinin bloklanmasını aktivləşdir"),
("id_input_tip", "ID, birbaşa IP və ya portla birlikdə domen (<domain>:<port>) daxil edə bilərsiniz.\nBaşqa serverdəki cihaza giriş etmək istəyirsinizsə, server ünvanını əlavə edin (<id>@<server_address>?key=<key_value>), məsələn,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nİctimai serverdəki cihaza giriş etmək istəyirsinizsə, \"<id>@public\" daxil edin, ictimai server üçün açar lazım deyil.\n\nİlk əlaqədə relay istifadəsini məcbur etmək istəyirsinizsə, ID-nin sonuna \"/r\" əlavə edin, məsələn, \"9123456234/r\"."),
("privacy_mode_impl_mag_tip", "Rejim 1"),
("privacy_mode_impl_virtual_display_tip", "Rejim 2"),
("Enter privacy mode", "Məxfilik rejiminə keç"),
("Exit privacy mode", "Məxfilik rejimindən çıx"),
("idd_not_support_under_win10_2004_tip", "Dolayı ekran sürücüsü dəstəklənmir. Windows 10, versiya 2004 və ya daha yenisi tələb olunur."),
("input_source_1_tip", "Giriş mənbəyi 1"),
("input_source_2_tip", "Giriş mənbəyi 2"),
("Swap control-command key", "Control və command düymələrini dəyişdir"),
("swap-left-right-mouse", "Sol və sağ siçan düymələrini dəyişdir"),
("2FA code", "2FA kodu"),
("More", "Daha çox"),
("enable-2fa-title", "İkifaktorlu doğrulamanı aktivləşdir"),
("enable-2fa-desc", "Doğrulayıcınızı indi qurun. Telefonunuzda və ya kompüterinizdə Authy, Microsoft və ya Google Authenticator kimi doğrulayıcı tətbiqdən istifadə edə bilərsiniz.\n\nQR kodu tətbiqinizlə skan edin və ikifaktorlu doğrulamanı aktivləşdirmək üçün tətbiqin göstərdiyi kodu daxil edin."),
("wrong-2fa-code", "Kod doğrulana bilmir. Kodun və lokal vaxt parametrlərinin düzgün olduğunu yoxlayın"),
("enter-2fa-title", "İkifaktorlu doğrulama"),
("Email verification code must be 6 characters.", "E-poçt doğrulama kodu 6 simvol olmalıdır."),
("2FA code must be 6 digits.", "2FA kodu 6 rəqəm olmalıdır."),
("Multiple Windows sessions found", "Bir neçə Windows sessiyası tapıldı"),
("Please select the session you want to connect to", "Qoşulmaq istədiyiniz sessiyanı seçin"),
("powered_by_me", "RustDesk ilə işləyir"),
("outgoing_only_desk_tip", "Bu, fərdiləşdirilmiş buraxılışdır.\nSiz digər cihazlara qoşula bilərsiniz, lakin digər cihazlar sizin cihazınıza qoşula bilməz."),
("preset_password_warning", "Bu fərdiləşdirilmiş buraxılış öncədən təyin edilmiş parolla gəlir. Bu parolu bilən hər kəs cihazınıza tam nəzarət edə bilər. Bunu gözləmirdinizsə, proqramı dərhal silin."),
("Security Alert", "Təhlükəsizlik xəbərdarlığı"),
("My address book", "Ünvan kitabım"),
("Personal", "Şəxsi"),
("Owner", "Sahib"),
("Set shared password", "Paylaşılan parolu təyin et"),
("Exist in", "Mövcuddur"),
("Read-only", "Yalnız oxu"),
("Read/Write", "Oxu/Yaz"),
("Full Control", "Tam nəzarət"),
("share_warning_tip", "Yuxarıdakı sahələr paylaşılır və başqaları tərəfindən görünür."),
("Everyone", "Hər kəs"),
("ab_web_console_tip", "Ətraflı məlumat veb konsolda"),
("allow-only-conn-window-open-tip", "Əlaqəyə yalnız RustDesk pəncərəsi açıq olduqda icazə ver"),
("no_need_privacy_mode_no_physical_displays_tip", "Fiziki ekran yoxdur, məxfilik rejiminə ehtiyac yoxdur."),
("Follow remote cursor", "Uzaq kursoru izlə"),
("Follow remote window focus", "Uzaq pəncərənin fokusunu izlə"),
("default_proxy_tip", "Standart protokol və port Socks5 və 1080-dir"),
("no_audio_input_device_tip", "Audio giriş cihazı tapılmadı."),
("Incoming", "Gələn"),
("Outgoing", "Gedən"),
("Clear Wayland screen selection", "Wayland ekran seçimini təmizlə"),
("clear_Wayland_screen_selection_tip", "Ekran seçimini təmizlədikdən sonra paylaşılacaq ekranı yenidən seçə bilərsiniz."),
("confirm_clear_Wayland_screen_selection_tip", "Wayland ekran seçimini təmizləmək istədiyinizə əminsiniz?"),
("android_new_voice_call_tip", "Yeni səsli zəng sorğusu alındı. Qəbul etsəniz, audio səsli ünsiyyətə keçəcək."),
("texture_render_tip", "Şəkillərin daha hamar olması üçün tekstura renderindən istifadə edin. Render problemləri ilə qarşılaşsanız bu seçimi söndürməyi sınaya bilərsiniz."),
("Use texture rendering", "Tekstura renderindən istifadə et"),
("Floating window", "Üzən pəncərə"),
("floating_window_tip", "RustDesk fon xidmətini işlək saxlamağa kömək edir"),
("Keep screen on", "Ekranııq saxla"),
("Never", "Heç vaxt"),
("During controlled", "İdarə olunarkən"),
("During service is on", "Xidmət işləyərkən"),
("Capture screen using DirectX", "Ekranı DirectX ilə çək"),
("Back", "Geri"),
("Apps", "Tətbiqlər"),
("Volume up", "Səsi artır"),
("Volume down", "Səsi azalt"),
("Power", "Güc"),
("Telegram bot", "Telegram botu"),
("enable-bot-tip", "Bu funksiyanı aktivləşdirsəniz, 2FA kodunu botunuzdan ala bilərsiniz. O, həm də əlaqə bildirişi kimi işləyə bilər."),
("enable-bot-desc", "1. @BotFather ilə söhbət açın.\n2. \"/newbot\" əmrini göndərin. Bu addımı tamamladıqdan sonra token alacaqsınız.\n3. Yeni yaratdığınız botla söhbətə başlayın. Onu aktivləşdirmək üçün \"/hello\" kimi kəsik xətt (\"/\") ilə başlayan mesaj göndərin.\n"),
("cancel-2fa-confirm-tip", "2FA-nı ləğv etmək istədiyinizə əminsiniz?"),
("cancel-bot-confirm-tip", "Telegram botunu ləğv etmək istədiyinizə əminsiniz?"),
("About RustDesk", "RustDesk haqqında"),
("Send clipboard keystrokes", "Mübadilə buferi düymə vurmalarını göndər"),
("network_error_tip", "Şəbəkə əlaqənizi yoxlayın, sonra yenidən cəhd düyməsini basın."),
("Unlock with PIN", "PIN ilə kilidi aç"),
("Requires at least {} characters", "Ən azı {} simvol tələb olunur"),
("Wrong PIN", "Yanlış PIN"),
("Set PIN", "PIN təyin et"),
("Enable trusted devices", "Etibarlı cihazları aktivləşdir"),
("Manage trusted devices", "Etibarlı cihazları idarə et"),
("Platform", "Platforma"),
("Days remaining", "Qalan günlər"),
("enable-trusted-devices-tip", "Etibarlı cihazlarda 2FA doğrulamasını keç"),
("Parent directory", "Yuxarı qovluq"),
("Resume", "Davam et"),
("Invalid file name", "Yanlış fayl adı"),
("one-way-file-transfer-tip", "İdarə olunan tərəfdə birtərəfli fayl ötürülməsi aktivdir."),
("Authentication Required", "Doğrulama tələb olunur"),
("Authenticate", "Doğrula"),
("web_id_input_tip", "Eyni serverdəki ID-ni daxil edə bilərsiniz, veb klientdə birbaşa IP girişi dəstəklənmir.\nBaşqa serverdəki cihaza giriş etmək istəyirsinizsə, server ünvanını əlavə edin (<id>@<server_address>?key=<key_value>), məsələn,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nİctimai serverdəki cihaza giriş etmək istəyirsinizsə, \"<id>@public\" daxil edin, ictimai server üçün açar lazım deyil."),
("Download", "Endir"),
("Upload folder", "Qovluq yüklə"),
("Upload files", "Fayl yüklə"),
("Clipboard is synchronized", "Mübadilə buferi sinxronlaşdırılıb"),
("Update client clipboard", "Klientin mübadilə buferini yenilə"),
("Untagged", "Etiketsiz"),
("new-version-of-{}-tip", "{} proqramının yeni versiyası mövcuddur"),
("Accessible devices", "Əlçatan cihazlar"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Uzaq tərəfdə RustDesk klientini {} və ya daha yeni versiyaya yüksəldin!"),
("d3d_render_tip", "D3D render aktiv olduqda bəzi kompüterlərdə uzaq idarəetmə ekranı qara ola bilər."),
("Use D3D rendering", "D3D renderindən istifadə et"),
("Printer", "Printer"),
("printer-os-requirement-tip", "Gedən çap funksiyası Windows 10 və ya daha yuxarı versiya tələb edir."),
("printer-requires-installed-{}-client-tip", "Uzaqdan çapdan istifadə etmək üçün bu cihazda {} quraşdırılmalıdır."),
("printer-{}-not-installed-tip", "{} Printeri quraşdırılmayıb."),
("printer-{}-ready-tip", "{} Printeri quraşdırılıb və istifadəyə hazırdır."),
("Install {} Printer", "{} Printerini quraşdır"),
("Outgoing Print Jobs", "Gedən çap tapşırıqları"),
("Incoming Print Jobs", "Gələn çap tapşırıqları"),
("Incoming Print Job", "Gələn çap tapşırığı"),
("use-the-default-printer-tip", "Standart printerdən istifadə et"),
("use-the-selected-printer-tip", "Seçilmiş printerdən istifadə et"),
("auto-print-tip", "Seçilmiş printerlə avtomatik çap et."),
("print-incoming-job-confirm-tip", "Uzaq tərəfdən çap tapşırığı aldınız. Onu öz tərəfinizdə icra etmək istəyirsiniz?"),
("remote-printing-disallowed-tile-tip", "Uzaqdan çapa icazə verilmir"),
("remote-printing-disallowed-text-tip", "İdarə olunan tərəfin icazə parametrləri uzaqdan çapı qadağan edir."),
("save-settings-tip", "Parametrləri yadda saxla"),
("dont-show-again-tip", "Bunu bir daha göstərmə"),
("Take screenshot", "Ekran görüntüsü al"),
("Taking screenshot", "Ekran görüntüsü alınır"),
("screenshot-merged-screen-not-supported-tip", "Bir neçə ekranın görüntüsünü birləşdirmək hazırda dəstəklənmir. Tək ekrana keçib yenidən cəhd edin."),
("screenshot-action-tip", "Ekran görüntüsü ilə necə davam edəcəyinizi seçin."),
("Save as", "Fərqli yadda saxla"),
("Export", "İxrac et"),
("Export Logs", "Jurnalları ixrac et"),
("Import Folder", "Qovluq idxal et"),
("Copy to clipboard", "Mübadilə buferinə kopyala"),
("Enable remote printer", "Uzaq printeri aktivləşdir"),
("Downloading {}", "{} endirilir"),
("{} Update", "{} yeniləməsi"),
("{}-to-update-tip", "{} indi bağlanacaq və yeni versiyanı quraşdıracaq."),
("download-new-version-failed-tip", "Endirmə alınmadı. Yenidən cəhd edə və ya \"Endir\" düyməsini basıb buraxılış səhifəsindən endirərək əl ilə yeniləyə bilərsiniz."),
("Auto update", "Avtomatik yeniləmə"),
("update-failed-check-msi-tip", "Quraşdırma üsulunun yoxlanışı alınmadı. \"Endir\" düyməsini basıb buraxılış səhifəsindən endirin və əl ilə yeniləyin."),
("websocket_tip", "WebSocket işlədilərkən yalnız relay əlaqələri dəstəklənir."),
("Use WebSocket", "WebSocket işlət"),
("Trackpad speed", "Trekped sürəti"),
("Default trackpad speed", "Standart trekped sürəti"),
("Numeric one-time password", "Rəqəmli birdəfəlik parol"),
("Enable IPv6 P2P connection", "IPv6 P2P əlaqəsini aktivləşdir"),
("Enable UDP hole punching", "UDP deşik açmanı aktivləşdir"),
("View camera", "Kameraya bax"),
("Enable camera", "Kameranı aktivləşdir"),
("No cameras", "Kamera yoxdur"),
("view_camera_unsupported_tip", "Uzaq cihaz kameraya baxışı dəstəkləmir."),
("Terminal", "Terminal"),
("Enable terminal", "Terminalı aktivləşdir"),
("New tab", "Yeni tab"),
("Keep terminal sessions on disconnect", "Əlaqə kəsiləndə terminal sessiyalarını saxla"),
("Terminal (Run as administrator)", "Terminal (Administrator kimi işə sal)"),
("terminal-admin-login-tip", "İdarə olunan tərəfin administrator istifadəçi adını və parolunu daxil edin."),
("Failed to get user token.", "İstifadəçi tokenini almaq alınmadı."),
("Incorrect username or password.", "Yanlış istifadəçi adı və ya parol."),
("The user is not an administrator.", "İstifadəçi administrator deyil."),
("Failed to check if the user is an administrator.", "İstifadəçinin administrator olduğunu yoxlamaq alınmadı."),
("Supported only in the installed version.", "Yalnız quraşdırılmış versiyada dəstəklənir."),
("elevation_username_tip", "İstifadəçi adını və ya domen\\istifadəçi_adı daxil edin"),
("Preparing for installation ...", "Quraşdırmaya hazırlanır ..."),
("Show my cursor", "Öz kursorumu göstər"),
("Scale custom", "Fərdi miqyas"),
("Custom scale slider", "Fərdi miqyas sürüşdürücüsü"),
("Decrease", "Azalt"),
("Increase", "Artır"),
("Show virtual mouse", "Virtual siçanı göstər"),
("Virtual mouse size", "Virtual siçanın ölçüsü"),
("Small", "Kiçik"),
("Large", "Böyük"),
("Show virtual joystick", "Virtual coystiki göstər"),
("Edit note", "Qeydi redaktə et"),
("Alias", "Ləqəb"),
("ScrollEdge", "Kənardan sürüşdürmə"),
("Allow insecure TLS fallback", "Təhlükəsiz olmayan TLS ehtiyat rejiminə icazə ver"),
("allow-insecure-tls-fallback-tip", "Standart olaraq RustDesk TLS işlədən protokollar üçün server sertifikatını yoxlayır.\nBu seçim aktiv olduqda, yoxlama alınmadığı halda RustDesk yoxlama addımını keçib davam edəcək."),
("Disable UDP", "UDP-ni söndür"),
("disable-udp-tip", "Yalnız TCP işlədilib işlədilməyəcəyini idarə edir.\nBu seçim aktiv olduqda RustDesk artıq UDP 21116 işlətməyəcək, əvəzində TCP 21116 işlədiləcək."),
("server-oss-not-support-tip", "QEYD: RustDesk server OSS bu funksiyanı əhatə etmir."),
("input note here", "qeydi buraya yazın"),
("note-at-conn-end-tip", "Əlaqə bitəndə qeyd soruş"),
("Show terminal extra keys", "Terminalın əlavə düymələrini göstər"),
("Relative mouse mode", "Nisbi siçan rejimi"),
("rel-mouse-not-supported-peer-tip", "Qoşulan qarşı tərəf nisbi siçan rejimini dəstəkləmir."),
("rel-mouse-not-ready-tip", "Nisbi siçan rejimi hələ hazır deyil. Yenidən cəhd edin."),
("rel-mouse-lock-failed-tip", "Kursoru kilidləmək alınmadı. Nisbi siçan rejimi söndürüldü."),
("rel-mouse-exit-{}-tip", "Çıxmaq üçün {} basın."),
("rel-mouse-permission-lost-tip", "Klaviatura icazəsi geri alındı. Nisbi siçan rejimi söndürüldü."),
("Changelog", "Dəyişikliklər siyahısı"),
("keep-awake-during-outgoing-sessions-label", "Gedən sessiyalar zamanı ekranı oyaq saxla"),
("keep-awake-during-incoming-sessions-label", "Gələn sessiyalar zamanı ekranı oyaq saxla"),
("Continue with {}", "{} ilə davam et"),
("Display Name", "Görünən ad"),
("password-hidden-tip", "Daimi parol təyin edilib (gizlədilib)."),
("preset-password-in-use-tip", "Hazırda öncədən təyin edilmiş parol işlədilir."),
("Enable privacy mode", "Məxfilik rejimini aktivləşdir"),
("allow-remote-toolbar-docking-any-edge", "Uzaq alətlər panelinin pəncərənin istənilən kənarına birləşməsinə icazə ver"),
("API Token", "API tokeni"),
("Deploy", "Yerləşdir"),
("Custom ID (optional)", "Fərdi ID (istəyə bağlı)"),
("server_requires_deployment_tip", "Server bu cihazın açıq şəkildə yerləşdirilməsini tələb edir. İndi yerləşdirilsin?"),
("The server does not require explicit deployment.", "Server açıq yerləşdirmə tələb etmir."),
("Unknown response.", "Naməlum cavab."),
("wayland-keyboard-input-disabled-tip", "Klaviatura girişinə icazə verilsin?"),
("wayland-keyboard-input-consent-tip", "Bu uzaq kompüterdə yazdıqlarınızı (parollar da daxil olmaqla) oradakı digər tətbiqlər oxuya bilər."),
("wayland-keyboard-input-applies-to-tip", "Bu seçim buna aiddir:"),
("wayland-soft-keyboard-input-label", "Ekran klaviaturası girişi"),
("wayland-keyboard-input-reset-choice-tip", "Klaviatura girişi seçimini sıfırla"),
("remember-wayland-keyboard-choice-tip", "Bu uzaq kompüter üçün bir daha soruşma"),
("Why this happens", "Bu niyə baş verir"),
("Switch display", "Ekranı dəyiş"),
("Show monitor switch button on the main toolbar", "Monitor dəyişdirmə düyməsini əsas alətlər panelində göstər"),
("Show on the minimized toolbar", "Kiçildilmiş alətlər panelində göstər"),
("All monitors", "Bütün monitorlar"),
("#{} monitor", "#{} monitor"),
("conn-e2ee-unavailable-tip", "Uçdan-uca şifrələmə doğrulana bilmədi.\nUzaq cihaz hələ qurulma mərhələsində ola bilər. Sonra yenidən cəhd edin.\nBu təkrarlanırsa, server etibarsız ola bilər.\nYenə də davam edilsin?"),
("ID whitelisting", "ID ağ siyahısı"),
("Use ID whitelisting", "ID ağ siyahısından istifadə et"),
("id_whitelist_tip", "Yalnız ağ siyahıdakı ID-lər mənə giriş edə bilər"),
("id_whitelist_wildcard_tip", "Joker simvollar dəstəklənir: '*' istənilən sayda simvola, '?' isə tam bir simvola uyğun gəlir"),
("Invalid ID", "Yanlış ID"),
("Your ID is blocked by the peer", "ID-niz qarşı tərəf tərəfindən bloklanıb"),
("Your ip is blocked by the peer", "IP-niz qarşı tərəf tərəfindən bloklanıb"),
("id_whitelist_caveat_tip", "ID qoşulan klient tərəfindən bildirilir. Bu ağ siyahı riski azaldır, lakin parolu və ya 2FA-nı əvəz etmir."),
("whitelist_cidr_tip", "CIDR yazılışı dəstəklənir, məsələn 192.168.1.0/24"),
("Continue", "Davam et"),
("Browser didn't open? Use the url below to sign in.", "Brauzer açılmadı? Daxil olmaq üçün aşağıdakı URL-dən istifadə edin."),
("Lock canvas", "Kətanı kilidlə"),
("Sync clipboard between sessions", "Mübadilə buferini sessiyalar arasında sinxronlaşdır"),
("sync-clipboard-between-sessions-tip", "Bir uzaq sessiyada kopyalanan mətn və ya şəkillər qoşulu olduğunuz digər sessiyaların mübadilə buferinə də göndərilir."),
("terminal-clipboard-write-tip", "Terminaldakı tətbiq bu cihazın mübadilə buferinə mətn kopyalamaq istəyir. İcazə versəniz, bu icazə siz onu Parametrlərdə söndürənə qədər bütün əlaqələrdəki terminal tətbiqlərinə şamil olunur. Əl ilə kopyalama və yapışdırma buna daxil deyil."),
("Allow terminal apps to copy to clipboard", "Terminal tətbiqlərinə mübadilə buferinə kopyalamağa icazə ver"),
("Enable", "Aktivləşdir"),
("Reuse one connection for port forwarding", "Port yönləndirmə üçün bir əlaqəni təkrar işlət"),
("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"),
("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."),
].iter().cloned().collect();
}

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Выкарыстоўваць падключэнне WebRTC P2P"), ("Enable WebRTC P2P connection", "Выкарыстоўваць падключэнне WebRTC P2P"),
("Enable TCP hole punching", "Выкарыстоўваць TCP hole punching"), ("Enable TCP hole punching", "Выкарыстоўваць TCP hole punching"),
("The screen sharing request was declined on the remote device", "Запыт на абагульванне экрана быў адхілены на аддаленай прыладзе"), ("The screen sharing request was declined on the remote device", "Запыт на абагульванне экрана быў адхілены на аддаленай прыладзе"),
("The screen sharing request timed out on the remote device", "Час чакання запыту на абагульванне экрана на аддаленай прыладзе выйшаў"), ("No one responded to the screen sharing request 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"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal завяршыў запыт на абагульванне экрана ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Партал працоўнага стала на аддаленай прыладзе не мае магчымасці, патрэбнай для абагульвання экрана або аддаленага кіравання, магчыма не ўсталяваны яго бэкенд"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal не вярнуў экран для захопу, магчыма бібліятэка PipeWire занадта старая"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Абагульванне экрана было дазволена на аддаленай прыладзе, але не ўдалося адкрыць злучэнне PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "Адсутнічае плагін GStreamer, патрэбны для захопу экрана ({})"),
("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 секунды."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Позволяване на WebRTC P2P връзка"), ("Enable WebRTC P2P connection", "Позволяване на WebRTC P2P връзка"),
("Enable TCP hole punching", "Позволяване на TCP hole punching"), ("Enable TCP hole punching", "Позволяване на TCP hole punching"),
("The screen sharing request was declined on the remote device", "Заявката за споделяне на екрана беше отхвърлена на отдалеченото устройство"), ("The screen sharing request was declined on the remote device", "Заявката за споделяне на екрана беше отхвърлена на отдалеченото устройство"),
("The screen sharing request timed out on the remote device", "Заявката за споделяне на екрана на отдалеченото устройство изтече"), ("No one responded to the screen sharing request 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 може да я използва"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal прекрати заявката за споделяне на екрана ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Порталът на работния плот на отдалеченото устройство няма възможност, необходима за споделяне на екрана или отдалечено управление, може да липсва неговата реализация"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal не върна екран за заснемане, библиотеката PipeWire може да е твърде стара"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Споделянето на екрана беше одобрено на отдалеченото устройство, но връзката с PipeWire не можа да бъде отворена"), ("A GStreamer plugin needed for screen capture is missing ({})", "Липсва приставка на GStreamer, необходима за заснемане на екрана ({})"),
("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 секунди."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Habilita la connexió WebRTC P2P"), ("Enable WebRTC P2P connection", "Habilita la connexió WebRTC P2P"),
("Enable TCP hole punching", "Activa la perforació TCP"), ("Enable TCP hole punching", "Activa la perforació TCP"),
("The screen sharing request was declined on the remote device", "La sol·licitud de compartició de pantalla s'ha rebutjat al dispositiu remot"), ("The screen sharing request was declined on the remote device", "La sol·licitud de compartició de pantalla s'ha rebutjat al dispositiu remot"),
("The screen sharing request timed out on the remote device", "La sol·licitud de compartició de pantalla ha esgotat el temps al dispositiu remot"), ("No one responded to the screen sharing request on the remote device", "Ningú no ha respost a la sol·licitud de compartició de pantalla al dispositiu remot"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "El RustDesk no pot accedir a la sessió d'escriptori del dispositiu remot; comproveu que hi ha una sessió en marxa i que el RustDesk hi pot accedir"), ("The XDG Desktop Portal ended the screen sharing request ({})", "L'XDG Desktop Portal ha finalitzat la sol·licitud de compartició de pantalla ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Al portal d'escriptori del dispositiu remot li falta una funcionalitat necessària per compartir la pantalla o per al control remot; potser no té cap implementació instal·lada"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "L'XDG Desktop Portal no ha retornat cap pantalla per capturar; la biblioteca PipeWire pot ser massa antiga"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "S'ha aprovat la compartició de pantalla al dispositiu remot, però no s'ha pogut obrir la connexió PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "Falta un connector del GStreamer necessari per capturar la pantalla ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -770,15 +770,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("port-forward-mux-tip", "同一条端口转发规则上的所有连接共用一条到对方的连接,而不是每条连接都重新连接并登录一次。"), ("port-forward-mux-tip", "同一条端口转发规则上的所有连接共用一条到对方的连接,而不是每条连接都重新连接并登录一次。"),
("Enable WebRTC P2P connection", "启用 WebRTC P2P 连接"), ("Enable WebRTC P2P connection", "启用 WebRTC P2P 连接"),
("Enable TCP hole punching", "启用 TCP 打洞"), ("Enable TCP hole punching", "启用 TCP 打洞"),
("The screen sharing request was declined on the remote device", "远程设备上的用户拒绝了屏幕共享请求"), ("The screen sharing request was declined on the remote device", "远程设备上拒绝了屏幕共享请求"),
("The screen sharing request timed out on the remote device", "远程设备上屏幕共享请求超时了"), ("No one responded to the screen sharing request 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 可以使用它"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal 结束了屏幕共享请求 ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "远程设备上的桌面门户缺少屏幕共享或远程控制所需的功能,可能没有安装它的后端"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal 未返回可捕获的屏幕PipeWire 库可能过旧"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "远程设备上已批准屏幕共享,但无法打开 PipeWire 连接"), ("A GStreamer plugin needed for screen capture is missing ({})", "缺少屏幕捕获所需的 GStreamer 插件 ({})"),
("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 秒。"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Povolit připojení WebRTC P2P"), ("Enable WebRTC P2P connection", "Povolit připojení WebRTC P2P"),
("Enable TCP hole punching", "Povolit TCP hole punching"), ("Enable TCP hole punching", "Povolit TCP hole punching"),
("The screen sharing request was declined on the remote device", "Žádost o sdílení obrazovky byla na vzdáleném zařízení odmítnuta"), ("The screen sharing request was declined on the remote device", "Žádost o sdílení obrazovky byla na vzdáleném zařízení odmítnuta"),
("The screen sharing request timed out on the remote device", "Vypršel časový limit žádosti o sdílení obrazovky na vzdáleném zařízení"), ("No one responded to the screen sharing request on the remote device", "Na žádost o sdílení obrazovky na vzdáleném zařízení nikdo neodpověděl"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk nemůže získat přístup k relaci plochy na vzdáleném zařízení, ověřte, že relace běží a že ji RustDesk může použít"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal ukončil žádost o sdílení obrazovky ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Portálu plochy na vzdáleném zařízení chybí funkce potřebná pro sdílení obrazovky nebo vzdálené ovládání, jeho implementace možná není nainstalována"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal nevrátil žádnou obrazovku k zachycení, knihovna PipeWire může být příliš stará"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Sdílení obrazovky bylo na vzdáleném zařízení schváleno, ale připojení PipeWire se nepodařilo otevřít"), ("A GStreamer plugin needed for screen capture is missing ({})", "Chybí zásuvný modul GStreamer potřebný k zachycení obrazovky ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Aktivér WebRTC P2P-forbindelse"), ("Enable WebRTC P2P connection", "Aktivér WebRTC P2P-forbindelse"),
("Enable TCP hole punching", "Aktivér TCP hole punching"), ("Enable TCP hole punching", "Aktivér TCP hole punching"),
("The screen sharing request was declined on the remote device", "Anmodningen om skærmdeling blev afvist på fjernenheden"), ("The screen sharing request was declined on the remote device", "Anmodningen om skærmdeling blev afvist på fjernenheden"),
("The screen sharing request timed out on the remote device", "Anmodningen om skærmdeling fik timeout på fjernenheden"), ("No one responded to the screen sharing request on the remote device", "Ingen svarede på anmodningen om skærmdeling på fjernenheden"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk kan ikke nå skrivebordssessionen på fjernenheden, kontrollér at en session kører, og at RustDesk kan bruge den"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal afsluttede anmodningen om skærmdeling ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Skrivebordsportalen på fjernenheden mangler en funktion, der kræves til skærmdeling eller fjernstyring, dens backend er måske ikke installeret"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal returnerede ingen skærm at optage, PipeWire-biblioteket er måske for gammelt"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Skærmdeling blev godkendt på fjernenheden, men PipeWire-forbindelsen kunne ikke åbnes"), ("A GStreamer plugin needed for screen capture is missing ({})", "Et GStreamer-plugin, der kræves til skærmoptagelse, mangler ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC-P2P-Verbindung aktivieren"), ("Enable WebRTC P2P connection", "WebRTC-P2P-Verbindung aktivieren"),
("Enable TCP hole punching", "TCP-Hole-Punching aktivieren"), ("Enable TCP hole punching", "TCP-Hole-Punching aktivieren"),
("The screen sharing request was declined on the remote device", "Die Anfrage zur Bildschirmfreigabe wurde auf dem entfernten Gerät abgelehnt"), ("The screen sharing request was declined on the remote device", "Die Anfrage zur Bildschirmfreigabe wurde auf dem entfernten Gerät abgelehnt"),
("The screen sharing request timed out on the remote device", "Bei der Anfrage zur Bildschirmfreigabe auf dem entfernten Gerät ist eine Zeitüberschreitung aufgetreten"), ("No one responded to the screen sharing request on the remote device", "Niemand hat auf die Anfrage zur Bildschirmfreigabe auf dem entfernten Gerät geantwortet"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk kann die Desktop-Sitzung auf dem entfernten Gerät nicht erreichen. Prüfen Sie, ob eine Sitzung läuft und ob RustDesk sie nutzen kann"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal hat die Anfrage zur Bildschirmfreigabe beendet ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Dem Desktop-Portal auf dem entfernten Gerät fehlt eine für Bildschirmfreigabe oder Fernsteuerung benötigte Fähigkeit, sein Backend ist möglicherweise nicht installiert"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal hat keinen Bildschirm zur Aufnahme zurückgegeben, die PipeWire-Bibliothek ist möglicherweise zu alt"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Die Bildschirmfreigabe wurde auf dem entfernten Gerät genehmigt, aber die PipeWire-Verbindung konnte nicht geöffnet werden"), ("A GStreamer plugin needed for screen capture is missing ({})", "Ein für die Bildschirmaufnahme benötigtes GStreamer-Plugin fehlt ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Ενεργοποίηση σύνδεσης WebRTC P2P"), ("Enable WebRTC P2P connection", "Ενεργοποίηση σύνδεσης WebRTC P2P"),
("Enable TCP hole punching", "Ενεργοποίηση διάτρησης οπών TCP"), ("Enable TCP hole punching", "Ενεργοποίηση διάτρησης οπών TCP"),
("The screen sharing request was declined on the remote device", "Το αίτημα κοινής χρήσης οθόνης απορρίφθηκε στην απομακρυσμένη συσκευή"), ("The screen sharing request was declined on the remote device", "Το αίτημα κοινής χρήσης οθόνης απορρίφθηκε στην απομακρυσμένη συσκευή"),
("The screen sharing request timed out on the remote device", "Το αίτημα κοινής χρήσης οθόνης έληξε στην απομακρυσμένη συσκευή"), ("No one responded to the screen sharing request 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 μπορεί να τη χρησιμοποιήσει"), ("The XDG Desktop Portal ended the screen sharing request ({})", "Το XDG Desktop Portal τερμάτισε το αίτημα κοινής χρήσης οθόνης ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Στην πύλη επιφάνειας εργασίας της απομακρυσμένης συσκευής λείπει μια δυνατότητα που απαιτείται για κοινή χρήση οθόνης ή απομακρυσμένο έλεγχο, ίσως δεν είναι εγκατεστημένο το υποσύστημά της"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "Το XDG Desktop Portal δεν επέστρεψε οθόνη για καταγραφή, η βιβλιοθήκη PipeWire ίσως είναι πολύ παλιά"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Η κοινή χρήση οθόνης εγκρίθηκε στην απομακρυσμένη συσκευή, αλλά δεν ήταν δυνατό το άνοιγμα της σύνδεσης PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "Λείπει ένα πρόσθετο GStreamer που απαιτείται για την καταγραφή οθόνης ({})"),
("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 δευτερολέπτων."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -278,6 +278,5 @@ 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."), ("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."), ("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."), ("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(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Ebligi WebRTC P2P-konekton"), ("Enable WebRTC P2P connection", "Ebligi WebRTC P2P-konekton"),
("Enable TCP hole punching", "Ebligi TCP-trapikadon"), ("Enable TCP hole punching", "Ebligi TCP-trapikadon"),
("The screen sharing request was declined on the remote device", "La peto pri ekrandividado estis rifuzita sur la fora aparato"), ("The screen sharing request was declined on the remote device", "La peto pri ekrandividado estis rifuzita sur la fora aparato"),
("The screen sharing request timed out on the remote device", "La peto pri ekrandividado eltempiĝis sur la fora aparato"), ("No one responded to the screen sharing request on the remote device", "Neniu respondis al la peto pri ekrandividado sur la fora aparato"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk ne povas atingi la labortablan seancon sur la fora aparato, kontrolu ke seanco funkcias kaj ke RustDesk povas uzi ĝin"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal finis la peton pri ekrandividado ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Al la labortabla portalo sur la fora aparato mankas kapablo necesa por ekrandividado aŭ fora regado, ĝia realigo eble ne estas instalita"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal redonis neniun ekranon por kapti, la biblioteko PipeWire eble estas tro malnova"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Ekrandividado estis aprobita sur la fora aparato, sed la konekto PipeWire ne malfermiĝis"), ("A GStreamer plugin needed for screen capture is missing ({})", "Mankas kromprogramo de GStreamer necesa por ekrankapto ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Habilitar conexión WebRTC P2P"), ("Enable WebRTC P2P connection", "Habilitar conexión WebRTC P2P"),
("Enable TCP hole punching", "Habilitar perforación de agujero TCP"), ("Enable TCP hole punching", "Habilitar perforación de agujero TCP"),
("The screen sharing request was declined on the remote device", "La solicitud de compartir pantalla fue rechazada en el dispositivo remoto"), ("The screen sharing request was declined on the remote device", "La solicitud de compartir pantalla fue rechazada en el dispositivo remoto"),
("The screen sharing request timed out on the remote device", "La solicitud de compartir pantalla ha agotado el tiempo de espera en el dispositivo remoto"), ("No one responded to the screen sharing request on the remote device", "Nadie respondió a la solicitud de compartir pantalla en el 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", "RustDesk no puede acceder a la sesión de escritorio del dispositivo remoto; compruebe que hay una sesión en marcha y que RustDesk puede usarla"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal finalizó la solicitud de compartir pantalla ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Al portal de escritorio del dispositivo remoto le falta una función necesaria para compartir la pantalla o para el control remoto; puede que no tenga instalada su implementación"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal no devolvió ninguna pantalla para capturar; la biblioteca PipeWire puede ser demasiado antigua"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Se aprobó compartir la pantalla en el dispositivo remoto, pero no se pudo abrir la conexión PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "Falta un complemento de GStreamer necesario para capturar la pantalla ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Luba WebRTC P2P-ühendus"), ("Enable WebRTC P2P connection", "Luba WebRTC P2P-ühendus"),
("Enable TCP hole punching", "Luba TCP-augustamine"), ("Enable TCP hole punching", "Luba TCP-augustamine"),
("The screen sharing request was declined on the remote device", "Ekraani jagamise taotlus lükati kaugseadmes tagasi"), ("The screen sharing request was declined on the remote device", "Ekraani jagamise taotlus lükati kaugseadmes tagasi"),
("The screen sharing request timed out on the remote device", "Ekraani jagamise taotlus aegus kaugseadmes"), ("No one responded to the screen sharing request on the remote device", "Keegi ei vastanud kaugseadmes ekraani jagamise taotlusele"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk ei pääse kaugseadmes töölauaseansini, kontrollige, kas seanss töötab ja kas RustDesk saab seda kasutada"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal lõpetas ekraani jagamise taotluse ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Kaugseadme töölauaportaalil puudub ekraani jagamiseks või kaugjuhtimiseks vajalik võimalus, selle taustarakendus ei pruugi olla paigaldatud"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal ei tagastanud ühtegi jäädvustatavat ekraani, PipeWire'i teek võib olla liiga vana"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Ekraani jagamine kiideti kaugseadmes heaks, kuid PipeWire'i ühendust ei õnnestunud avada"), ("A GStreamer plugin needed for screen capture is missing ({})", "Ekraani jäädvustamiseks vajalik GStreameri plugin puudub ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Gaitu WebRTC P2P konexioa"), ("Enable WebRTC P2P connection", "Gaitu WebRTC P2P konexioa"),
("Enable TCP hole punching", "Gaitu TCP zulo-egitea"), ("Enable TCP hole punching", "Gaitu TCP zulo-egitea"),
("The screen sharing request was declined on the remote device", "Pantaila partekatzeko eskaera baztertu egin da urruneko gailuan"), ("The screen sharing request was declined on the remote device", "Pantaila partekatzeko eskaera baztertu egin da urruneko gailuan"),
("The screen sharing request timed out on the remote device", "Pantaila partekatzeko eskaerak denbora-muga gainditu du urruneko gailuan"), ("No one responded to the screen sharing request on the remote device", "Inork ez du erantzun urruneko gailuko pantaila partekatzeko eskaerari"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk-ek ezin du urruneko gailuko mahaigaineko saioa atzitu, egiaztatu saio bat martxan dagoela eta RustDesk-ek erabil dezakeela"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal-ek pantaila partekatzeko eskaera amaitu du ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Urruneko gailuko mahaigaineko atariari pantaila partekatzeko edo urrunetik kontrolatzeko behar den gaitasun bat falta zaio, agian ez dago haren backend-a instalatuta"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal-ek ez du kapturatzeko pantailarik itzuli, PipeWire liburutegia zaharregia izan daiteke"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Pantaila partekatzea onartu da urruneko gailuan, baina ezin izan da PipeWire konexioa ireki"), ("A GStreamer plugin needed for screen capture is missing ({})", "Pantaila kapturatzeko beharrezkoa den GStreamer plugina falta da ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "فعال‌سازی اتصال همتا‌به‌همتای WebRTC"), ("Enable WebRTC P2P connection", "فعال‌سازی اتصال همتا‌به‌همتای WebRTC"),
("Enable TCP hole punching", "فعال‌سازی تکنیک TCP hole punching"), ("Enable TCP hole punching", "فعال‌سازی تکنیک TCP hole punching"),
("The screen sharing request was declined on the remote device", "درخواست اشتراک‌گذاری صفحه در دستگاه راه دور رد شد"), ("The screen sharing request was declined on the remote device", "درخواست اشتراک‌گذاری صفحه در دستگاه راه دور رد شد"),
("The screen sharing request timed out on the remote device", "مهلت درخواست اشتراک‌گذاری صفحه در دستگاه راه دور به پایان رسید"), ("No one responded to the screen sharing request 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 بتواند از آن استفاده کند"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal درخواست اشتراک‌گذاری صفحه را پایان داد ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "درگاه میزکار در دستگاه راه دور قابلیت لازم برای اشتراک‌گذاری صفحه یا کنترل از راه دور را ندارد، شاید پیاده‌سازی آن نصب نشده باشد"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal هیچ صفحه‌ای برای ضبط بازنگرداند، ممکن است کتابخانه PipeWire خیلی قدیمی باشد"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "اشتراک‌گذاری صفحه در دستگاه راه دور تأیید شد، اما اتصال PipeWire باز نشد"), ("A GStreamer plugin needed for screen capture is missing ({})", "افزونه GStreamer موردنیاز برای ضبط صفحه موجود نیست ({})"),
("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 ثانیه خالی بگذارید."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Ota WebRTC P2P yhteys käyttöön"), ("Enable WebRTC P2P connection", "Ota WebRTC P2P yhteys käyttöön"),
("Enable TCP hole punching", "Ota käyttöön TCP hole punching tekniikka"), ("Enable TCP hole punching", "Ota käyttöön TCP hole punching tekniikka"),
("The screen sharing request was declined on the remote device", "Näytön jakamispyyntö hylättiin etälaitteessa"), ("The screen sharing request was declined on the remote device", "Näytön jakamispyyntö hylättiin etälaitteessa"),
("The screen sharing request timed out on the remote device", "Näytön jakamispyyntö aikakatkaistiin etälaitteessa"), ("No one responded to the screen sharing request on the remote device", "Kukaan ei vastannut näytön jakamispyyntöön etälaitteessa"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk ei tavoita etälaitteen työpöytäistuntoa, tarkista että istunto on käynnissä ja että RustDesk voi käyttää sitä"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal päätti näytön jakamispyynnön ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Etälaitteen työpöytäportaalista puuttuu näytön jakamiseen tai etäohjaukseen tarvittava ominaisuus, sen taustaosaa ei ehkä ole asennettu"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal ei palauttanut kaapattavaa näyttöä, PipeWire-kirjasto voi olla liian vanha"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Näytön jakaminen hyväksyttiin etälaitteessa, mutta PipeWire-yhteyttä ei voitu avata"), ("A GStreamer plugin needed for screen capture is missing ({})", "Näytön kaappaukseen tarvittava GStreamer-liitännäinen puuttuu ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Activer la connexion P2P WebRTC"), ("Enable WebRTC P2P connection", "Activer la connexion P2P WebRTC"),
("Enable TCP hole punching", "Activer le « hole punching » TCP"), ("Enable TCP hole punching", "Activer le « hole punching » TCP"),
("The screen sharing request was declined on the remote device", "La demande de partage d'écran a été refusée sur l'appareil distant"), ("The screen sharing request was declined on the remote device", "La demande de partage d'écran a été refusée sur l'appareil distant"),
("The screen sharing request timed out on the remote device", "La demande de partage d'écran a expiré sur l'appareil distant"), ("No one responded to the screen sharing request on the remote device", "Personne n'a répondu à la demande de partage d'écran sur l'appareil distant"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk ne peut pas accéder à la session de bureau de l'appareil distant, vérifiez qu'une session est ouverte et que RustDesk peut l'utiliser"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal a mis fin à la demande de partage d'écran ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Il manque au portail de bureau de l'appareil distant une fonctionnalité nécessaire au partage d'écran ou au contrôle à distance, son backend n'est peut-être pas installé"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal n'a renvoyé aucun écran à capturer, la bibliothèque PipeWire est peut-être trop ancienne"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Le partage d'écran a été approuvé sur l'appareil distant, mais la connexion PipeWire n'a pas pu être ouverte"), ("A GStreamer plugin needed for screen capture is missing ({})", "Un greffon GStreamer nécessaire à la capture d'écran est manquant ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P კავშირის ჩართვა"), ("Enable WebRTC P2P connection", "WebRTC P2P კავშირის ჩართვა"),
("Enable TCP hole punching", "TCP hole punching-ის ჩართვა"), ("Enable TCP hole punching", "TCP hole punching-ის ჩართვა"),
("The screen sharing request was declined on the remote device", "ეკრანის გაზიარების მოთხოვნა უარყოფილია დისტანციურ მოწყობილობაზე"), ("The screen sharing request was declined on the remote device", "ეკრანის გაზიარების მოთხოვნა უარყოფილია დისტანციურ მოწყობილობაზე"),
("The screen sharing request timed out on the remote device", "ეკრანის გაზიარების მოთხოვნას ვადა გაუვიდა დისტანციურ მოწყობილობაზე"), ("No one responded to the screen sharing request 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-ს შეუძლია მისი გამოყენება"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal-მა დაასრულა ეკრანის გაზიარების მოთხოვნა ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "დისტანციური მოწყობილობის სამუშაო მაგიდის პორტალს აკლია ეკრანის გაზიარებისთვის ან დისტანციური მართვისთვის საჭირო შესაძლებლობა, შესაძლოა მისი ბექენდი დაინსტალირებული არ არის"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal-მა არ დააბრუნა ჩასაწერი ეკრანი, PipeWire-ის ბიბლიოთეკა შესაძლოა ძალიან ძველია"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "ეკრანის გაზიარება დამტკიცდა დისტანციურ მოწყობილობაზე, მაგრამ PipeWire-ის კავშირის გახსნა ვერ მოხერხდა"), ("A GStreamer plugin needed for screen capture is missing ({})", "ეკრანის ჩაწერისთვის საჭირო GStreamer-ის მოდული აკლია ({})"),
("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 წამისთვის."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Activar conexión P2P por WebRTC"), ("Enable WebRTC P2P connection", "Activar conexión P2P por WebRTC"),
("Enable TCP hole punching", "Activar perforación de portos TCP"), ("Enable TCP hole punching", "Activar perforación de portos TCP"),
("The screen sharing request was declined on the remote device", "A solicitude de compartir pantalla foi rexeitada no dispositivo remoto"), ("The screen sharing request was declined on the remote device", "A solicitude de compartir pantalla foi rexeitada no dispositivo remoto"),
("The screen sharing request timed out on the remote device", "A solicitude de compartir pantalla esgotou o tempo no dispositivo remoto"), ("No one responded to the screen sharing request on the remote device", "Ninguén respondeu á solicitude de compartir pantalla 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", "RustDesk non pode acceder á sesión de escritorio do dispositivo remoto, comprobe que hai unha sesión en marcha e que RustDesk pode usala"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal rematou a solicitude de compartir pantalla ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Ao portal de escritorio do dispositivo remoto fáltalle unha funcionalidade necesaria para compartir a pantalla ou para o control remoto, pode que non teña instalada a súa implementación"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal non devolveu ningunha pantalla para capturar, a biblioteca PipeWire pode ser demasiado antiga"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Aprobouse compartir a pantalla no dispositivo remoto, pero non se puido abrir a conexión PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "Falta un complemento de GStreamer necesario para capturar a pantalla ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P કનેક્શન સક્ષમ કરો"), ("Enable WebRTC P2P connection", "WebRTC P2P કનેક્શન સક્ષમ કરો"),
("Enable TCP hole punching", "TCP હોલ પંચિંગ સક્ષમ કરો"), ("Enable TCP hole punching", "TCP હોલ પંચિંગ સક્ષમ કરો"),
("The screen sharing request was declined on the remote device", "રિમોટ ઉપકરણ પર સ્ક્રીન શેરિંગ વિનંતી નકારવામાં આવી"), ("The screen sharing request was declined on the remote device", "રિમોટ ઉપકરણ પર સ્ક્રીન શેરિંગ વિનંતી નકારવામાં આવી"),
("The screen sharing request timed out on the remote device", "રિમોટ ઉપકરણ પર સ્ક્રીન શેરિંગ વિનંતીનો સમય સમાપ્ત થયો"), ("No one responded to the screen sharing request 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 તેનો ઉપયોગ કરી શકે છે"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal એ સ્ક્રીન શેરિંગ વિનંતી સમાપ્ત કરી ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "રિમોટ ઉપકરણ પરના ડેસ્કટોપ પોર્ટલમાં સ્ક્રીન શેરિંગ અથવા રિમોટ કંટ્રોલ માટે જરૂરી ક્ષમતા નથી, તેનું બેકએન્ડ કદાચ ઇન્સ્ટોલ કરેલું નથી"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal એ કૅપ્ચર કરવા માટે કોઈ સ્ક્રીન પરત કરી નથી, PipeWire લાઇબ્રેરી કદાચ ઘણી જૂની છે"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "રિમોટ ઉપકરણ પર સ્ક્રીન શેરિંગ મંજૂર થયું, પરંતુ PipeWire કનેક્શન ખોલી શકાયું નહીં"), ("A GStreamer plugin needed for screen capture is missing ({})", "સ્ક્રીન કૅપ્ચર માટે જરૂરી GStreamer પ્લગઇન ખૂટે છે ({})"),
("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 સેકન્ડ માટે ખાલી રાખો."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "אפשר חיבור WebRTC P2P"), ("Enable WebRTC P2P connection", "אפשר חיבור WebRTC P2P"),
("Enable TCP hole punching", "אפשר TCP hole punching"), ("Enable TCP hole punching", "אפשר TCP hole punching"),
("The screen sharing request was declined on the remote device", "בקשת שיתוף המסך נדחתה במכשיר המרוחק"), ("The screen sharing request was declined on the remote device", "בקשת שיתוף המסך נדחתה במכשיר המרוחק"),
("The screen sharing request timed out on the remote device", "תם הזמן המוקצב לבקשת שיתוף המסך במכשיר המרוחק"), ("No one responded to the screen sharing request 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 יכול להשתמש בה"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal סיים את בקשת שיתוף המסך ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "לפורטל שולחן העבודה במכשיר המרוחק חסרה יכולת הדרושה לשיתוף מסך או לשליטה מרחוק, ייתכן שהמימוש שלו אינו מותקן"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal לא החזיר מסך ללכידה, ייתכן שספריית PipeWire ישנה מדי"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "שיתוף המסך אושר במכשיר המרוחק, אך לא ניתן היה לפתוח את חיבור PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "חסר תוסף GStreamer הדרוש ללכידת מסך ({})"),
("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 שניות."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P कनेक्शन सक्षम करें"), ("Enable WebRTC P2P connection", "WebRTC P2P कनेक्शन सक्षम करें"),
("Enable TCP hole punching", "TCP होल पंचिंग सक्षम करें"), ("Enable TCP hole punching", "TCP होल पंचिंग सक्षम करें"),
("The screen sharing request was declined on the remote device", "रिमोट डिवाइस पर स्क्रीन शेयरिंग अनुरोध अस्वीकार कर दिया गया"), ("The screen sharing request was declined on the remote device", "रिमोट डिवाइस पर स्क्रीन शेयरिंग अनुरोध अस्वीकार कर दिया गया"),
("The screen sharing request timed out on the remote device", "रिमोट डिवाइस पर स्क्रीन शेयरिंग अनुरोध का समय समाप्त हो गया"), ("No one responded to the screen sharing request 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 उसका उपयोग कर सकता है"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal ने स्क्रीन शेयरिंग अनुरोध समाप्त कर दिया ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "रिमोट डिवाइस के डेस्कटॉप पोर्टल में स्क्रीन शेयरिंग या रिमोट कंट्रोल के लिए आवश्यक क्षमता नहीं है, शायद उसका बैकएंड इंस्टॉल नहीं है"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal ने कैप्चर करने के लिए कोई स्क्रीन नहीं लौटाई, PipeWire लाइब्रेरी बहुत पुरानी हो सकती है"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "रिमोट डिवाइस पर स्क्रीन शेयरिंग स्वीकृत हुई, लेकिन PipeWire कनेक्शन नहीं खोला जा सका"), ("A GStreamer plugin needed for screen capture is missing ({})", "स्क्रीन कैप्चर के लिए आवश्यक GStreamer प्लगइन अनुपस्थित है ({})"),
("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 सेकंड के लिए खाली छोड़ें।"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Omogući WebRTC P2P vezu"), ("Enable WebRTC P2P connection", "Omogući WebRTC P2P vezu"),
("Enable TCP hole punching", "Omogući TCP hole punching"), ("Enable TCP hole punching", "Omogući TCP hole punching"),
("The screen sharing request was declined on the remote device", "Zahtjev za dijeljenje zaslona odbijen je na udaljenom uređaju"), ("The screen sharing request was declined on the remote device", "Zahtjev za dijeljenje zaslona odbijen je na udaljenom uređaju"),
("The screen sharing request timed out on the remote device", "Zahtjev za dijeljenje zaslona istekao je na udaljenom uređaju"), ("No one responded to the screen sharing request on the remote device", "Nitko nije odgovorio na zahtjev za dijeljenje zaslona na udaljenom uređaju"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk ne može pristupiti sesiji radne površine na udaljenom uređaju, provjerite radi li sesija i može li je RustDesk koristiti"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal prekinuo je zahtjev za dijeljenje zaslona ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Portalu radne površine na udaljenom uređaju nedostaje mogućnost potrebna za dijeljenje zaslona ili daljinsko upravljanje, njegov pozadinski dio možda nije instaliran"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal nije vratio zaslon za snimanje, PipeWire biblioteka je možda prestara"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Dijeljenje zaslona odobreno je na udaljenom uređaju, ali PipeWire vezu nije bilo moguće otvoriti"), ("A GStreamer plugin needed for screen capture is missing ({})", "Nedostaje GStreamer dodatak potreban za snimanje zaslona ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P kapcsolat engedélyezése"), ("Enable WebRTC P2P connection", "WebRTC P2P kapcsolat engedélyezése"),
("Enable TCP hole punching", "TCP résszűrés engedélyezése"), ("Enable TCP hole punching", "TCP résszűrés engedélyezése"),
("The screen sharing request was declined on the remote device", "A képernyőmegosztási kérést elutasították a távoli eszközön"), ("The screen sharing request was declined on the remote device", "A képernyőmegosztási kérést elutasították a távoli eszközön"),
("The screen sharing request timed out on the remote device", "A képernyőmegosztási kérés időtúllépést okozott a távoli eszközön"), ("No one responded to the screen sharing request on the remote device", "Senki sem válaszolt a képernyőmegosztási kérésre a távoli eszközön"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "A RustDesk nem éri el az asztali munkamenetet a távoli eszközön, ellenőrizze, hogy fut-e munkamenet és hogy a RustDesk használhatja-e"), ("The XDG Desktop Portal ended the screen sharing request ({})", "Az XDG Desktop Portal befejezte a képernyőmegosztási kérést ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "A távoli eszköz asztali portáljából hiányzik a képernyőmegosztáshoz vagy távvezérléshez szükséges képesség, a háttérrendszere talán nincs telepítve"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "Az XDG Desktop Portal nem adott vissza rögzíthető képernyőt, a PipeWire programkönyvtár túl régi lehet"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "A képernyőmegosztást jóváhagyták a távoli eszközön, de a PipeWire-kapcsolatot nem sikerült megnyitni"), ("A GStreamer plugin needed for screen capture is missing ({})", "Hiányzik a képernyőrögzítéshez szükséges GStreamer bővítmény ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Aktifkan koneksi P2P WebRTC"), ("Enable WebRTC P2P connection", "Aktifkan koneksi P2P WebRTC"),
("Enable TCP hole punching", "Aktifkan TCP hole punching"), ("Enable TCP hole punching", "Aktifkan TCP hole punching"),
("The screen sharing request was declined on the remote device", "Permintaan berbagi layar ditolak di perangkat jarak jauh"), ("The screen sharing request was declined on the remote device", "Permintaan berbagi layar ditolak di perangkat jarak jauh"),
("The screen sharing request timed out on the remote device", "Permintaan berbagi layar kehabisan waktu di perangkat jarak jauh"), ("No one responded to the screen sharing request on the remote device", "Tidak ada yang menanggapi permintaan berbagi layar di perangkat jarak jauh"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk tidak dapat mengakses sesi desktop di perangkat jarak jauh, pastikan sesi desktop berjalan dan dapat digunakan oleh RustDesk"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal mengakhiri permintaan berbagi layar ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Portal desktop di perangkat jarak jauh tidak memiliki kemampuan yang diperlukan untuk berbagi layar atau kendali jarak jauh, backend-nya mungkin belum terpasang"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal tidak mengembalikan layar untuk direkam, pustaka PipeWire mungkin terlalu lama"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Berbagi layar disetujui di perangkat jarak jauh, tetapi koneksi PipeWire tidak dapat dibuka"), ("A GStreamer plugin needed for screen capture is missing ({})", "Plugin GStreamer yang diperlukan untuk merekam layar tidak ditemukan ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -541,7 +541,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Plug out all", "Scollega tutto"), ("Plug out all", "Scollega tutto"),
("True color (4:4:4)", "Colore reale (4:4:4)"), ("True color (4:4:4)", "Colore reale (4:4:4)"),
("Enable blocking user input", "Abilita blocco input utente"), ("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 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\"."), ("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\"."),
("privacy_mode_impl_mag_tip", "Modo 1"), ("privacy_mode_impl_mag_tip", "Modo 1"),
("privacy_mode_impl_virtual_display_tip", "Modo 2"), ("privacy_mode_impl_virtual_display_tip", "Modo 2"),
("Enter privacy mode", "Entra in modalità privacy"), ("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"), ("Parent directory", "Cartella principale"),
("Resume", "Riprendi"), ("Resume", "Riprendi"),
("Invalid file name", "Nome file non valido"), ("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"), ("Authentication Required", "Richiesta autenticazione"),
("Authenticate", "Autentica"), ("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."), ("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"), ("Default trackpad speed", "Velocità predefinita trackpad"),
("Numeric one-time password", "Password numerica monouso"), ("Numeric one-time password", "Password numerica monouso"),
("Enable IPv6 P2P connection", "Abilita connessione P2P IPv6"), ("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"), ("View camera", "Visualizza telecamera"),
("Enable camera", "Abilita camera"), ("Enable camera", "Abilita camera"),
("No cameras", "Nessuna camera"), ("No cameras", "Nessuna camera"),
@@ -769,16 +769,11 @@ 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 "), ("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."), ("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 WebRTC P2P connection", "Abilita connessione P2P WebRTC"),
("Enable TCP hole punching", "Abilita hole punching TCP"), ("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 was declined on the remote device", ""),
("The screen sharing request timed out on the remote device", "La richiesta di condivisione dello schermo nel dispositivo remoto è scaduta"), ("No one responded to the screen sharing request 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 non riesce a raggiungere la sessione desktop nel dispositivo remoto, controlla che sia in esecuzione una sessione desktop e che RustDesk possa usarla"), ("The XDG Desktop Portal ended the screen sharing request ({})", ""),
("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"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", ""),
("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"), ("A GStreamer plugin needed for screen capture is missing ({})", ""),
("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", ""),
("relay-fallback-delay-tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P 接続を有効化する"), ("Enable WebRTC P2P connection", "WebRTC P2P 接続を有効化する"),
("Enable TCP hole punching", "TCP ホールパンチを有効化する"), ("Enable TCP hole punching", "TCP ホールパンチを有効化する"),
("The screen sharing request was declined on the remote device", "リモート端末で画面共有の要求が拒否されました"), ("The screen sharing request was declined on the remote device", "リモート端末で画面共有の要求が拒否されました"),
("The screen sharing request timed out on the remote device", "リモート端末で画面共有の要求がタイムアウトしました"), ("No one responded to the screen sharing request 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 から利用できることを確認してください"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal が画面共有の要求を終了しました ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "リモート端末のデスクトップポータルに画面共有または遠隔操作に必要な機能がありません。バックエンドが未インストールの可能性があります"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal がキャプチャ対象の画面を返しませんでした。PipeWire ライブラリが古すぎる可能性があります"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "リモート端末で画面共有は許可されましたが、PipeWire 接続を開けませんでした"), ("A GStreamer plugin needed for screen capture is missing ({})", "画面キャプチャに必要な GStreamer プラグインがありません ({})"),
("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 秒になります。"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P 연결 사용"), ("Enable WebRTC P2P connection", "WebRTC P2P 연결 사용"),
("Enable TCP hole punching", "TCP 홀 펀칭 사용"), ("Enable TCP hole punching", "TCP 홀 펀칭 사용"),
("The screen sharing request was declined on the remote device", "원격 장치에서 화면 공유 요청이 거부되었습니다"), ("The screen sharing request was declined on the remote device", "원격 장치에서 화면 공유 요청이 거부되었습니다"),
("The screen sharing request timed out on the remote device", "원격 장치에서 화면 공유 요청이 시간 초과되었습니다"), ("No one responded to the screen sharing request 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가 사용할 수 있는지 확인하세요"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal이 화면 공유 요청을 종료했습니다 ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "원격 장치의 데스크톱 포털에 화면 공유 또는 원격 제어에 필요한 기능이 없습니다. 백엔드가 설치되지 않았을 수 있습니다"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal이 캡처할 화면을 반환하지 않았습니다. PipeWire 라이브러리가 너무 오래되었을 수 있습니다"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "원격 장치에서 화면 공유가 승인되었지만 PipeWire 연결을 열 수 없습니다"), ("A GStreamer plugin needed for screen capture is missing ({})", "화면 캡처에 필요한 GStreamer 플러그인이 없습니다 ({})"),
("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초가 사용됩니다."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P қосылымын іске қосу"), ("Enable WebRTC P2P connection", "WebRTC P2P қосылымын іске қосу"),
("Enable TCP hole punching", "TCP hole punching'ті іске қосу"), ("Enable TCP hole punching", "TCP hole punching'ті іске қосу"),
("The screen sharing request was declined on the remote device", "Қашықтағы құрылғыда экранды бөлісу сұрауы қабылданбады"), ("The screen sharing request was declined on the remote device", "Қашықтағы құрылғыда экранды бөлісу сұрауы қабылданбады"),
("The screen sharing request timed out on the remote device", "Қашықтағы құрылғыда экранды бөлісу сұрауының уақыты бітті"), ("No one responded to the screen sharing request 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 оны пайдалана алатынын тексеріңіз"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal экранды бөлісу сұрауын аяқтады ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Қашықтағы құрылғының жұмыс үстелі порталында экранды бөлісуге немесе қашықтан басқаруға қажет мүмкіндік жоқ, оның бэкенді орнатылмаған болуы мүмкін"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal түсіруге арналған экран қайтармады, PipeWire кітапханасы тым ескі болуы мүмкін"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Қашықтағы құрылғыда экранды бөлісуге рұқсат берілді, бірақ PipeWire байланысын ашу мүмкін болмады"), ("A GStreamer plugin needed for screen capture is missing ({})", "Экранды түсіру үшін қажет GStreamer плагині жоқ ({})"),
("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 секунд үшін бос қалдырыңыз."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Įgalinti WebRTC P2P ryšį"), ("Enable WebRTC P2P connection", "Įgalinti WebRTC P2P ryšį"),
("Enable TCP hole punching", "Įgalinti TCP gręžimą (hole punching)"), ("Enable TCP hole punching", "Įgalinti TCP gręžimą (hole punching)"),
("The screen sharing request was declined on the remote device", "Ekrano bendrinimo užklausa buvo atmesta nuotoliniame įrenginyje"), ("The screen sharing request was declined on the remote device", "Ekrano bendrinimo užklausa buvo atmesta nuotoliniame įrenginyje"),
("The screen sharing request timed out on the remote device", "Baigėsi ekrano bendrinimo užklausos laikas nuotoliniame įrenginyje"), ("No one responded to the screen sharing request on the remote device", "Niekas neatsakė į ekrano bendrinimo užklausą nuotoliniame įrenginyje"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk negali pasiekti nuotolinio įrenginio darbalaukio seanso, patikrinkite, ar seansas veikia ir ar RustDesk gali jį naudoti"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal užbaigė ekrano bendrinimo užklausą ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Nuotolinio įrenginio darbalaukio portalui trūksta ekrano bendrinimui ar nuotoliniam valdymui reikalingos galimybės, gali būti neįdiegta jo posistemė"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal negrąžino jokio ekrano įrašymui, PipeWire biblioteka gali būti per sena"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Ekrano bendrinimas nuotoliniame įrenginyje buvo patvirtintas, bet nepavyko atverti PipeWire ryšio"), ("A GStreamer plugin needed for screen capture is missing ({})", "Trūksta ekrano įrašymui reikalingo GStreamer papildinio ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Iespējot WebRTC P2P savienojumu"), ("Enable WebRTC P2P connection", "Iespējot WebRTC P2P savienojumu"),
("Enable TCP hole punching", "Iespējot TCP caurumu veidošanu"), ("Enable TCP hole punching", "Iespējot TCP caurumu veidošanu"),
("The screen sharing request was declined on the remote device", "Ekrāna koplietošanas pieprasījums attālinātajā ierīcē tika noraidīts"), ("The screen sharing request was declined on the remote device", "Ekrāna koplietošanas pieprasījums attālinātajā ierīcē tika noraidīts"),
("The screen sharing request timed out on the remote device", "Ekrāna koplietošanas pieprasījumam attālinātajā ierīcē iestājās noildze"), ("No one responded to the screen sharing request on the remote device", "Neviens neatbildēja uz ekrāna koplietošanas pieprasījumu attālinātajā ierīcē"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk nevar piekļūt attālinātās ierīces darbvirsmas sesijai, pārbaudiet, vai sesija darbojas un vai RustDesk to var izmantot"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal pārtrauca ekrāna koplietošanas pieprasījumu ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Attālinātās ierīces darbvirsmas portālam trūkst ekrāna koplietošanai vai attālinātai vadībai nepieciešamās iespējas, tā aizmugursisma varētu nebūt instalēta"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal neatgrieza nevienu tveramo ekrānu, PipeWire biblioka var būt pārāk veca"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Ekrāna koplietošana attālinātajā ierīcē tika apstiprināta, bet PipeWire savienojumu neizdevās atvērt"), ("A GStreamer plugin needed for screen capture is missing ({})", "Trūkst ekrāna tveršanai nepieciešamā GStreamer spraudņa ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P കണക്ഷൻ അനുവദിക്കുക"), ("Enable WebRTC P2P connection", "WebRTC P2P കണക്ഷൻ അനുവദിക്കുക"),
("Enable TCP hole punching", "TCP ഹോൾ പഞ്ചിംഗ് അനുവദിക്കുക"), ("Enable TCP hole punching", "TCP ഹോൾ പഞ്ചിംഗ് അനുവദിക്കുക"),
("The screen sharing request was declined on the remote device", "വിദൂര ഉപകരണത്തിൽ സ്ക്രീൻ പങ്കിടൽ അഭ്യർത്ഥന നിരസിച്ചു"), ("The screen sharing request was declined on the remote device", "വിദൂര ഉപകരണത്തിൽ സ്ക്രീൻ പങ്കിടൽ അഭ്യർത്ഥന നിരസിച്ചു"),
("The screen sharing request timed out on the remote device", "വിദൂര ഉപകരണത്തി സ്ക്രീൻ പങ്കിടൽ അഭ്യർത്ഥനയുടെ സമയം കഴിഞ്ഞു"), ("No one responded to the screen sharing request 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-ന് അത് ഉപയോഗിക്കാമെന്നും ഉറപ്പാക്കുക"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal സ്ക്രീൻ പങ്കിടൽ അഭ്യർത്ഥന അവസാനിപ്പിച്ചു ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "വിദൂര ഉപകരണത്തിലെ ഡെസ്ക്ടോപ്പ് പോർട്ടലിന് സ്ക്രീൻ പങ്കിടലിനോ വിദൂര നിയന്ത്രണത്തിനോ ആവശ്യമായ ശേഷിയില്ല, അതിന്റെ ബാക്കെൻഡ് ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ലായിരിക്കാം"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal പകർത്താൻ ഒരു സ്ക്രീനും നൽകിയില്ല, PipeWire ലൈബ്രറി വളരെ പഴയതാകാം"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "വിദൂര ഉപകരണത്തിൽ സ്ക്രീൻ പങ്കിടൽ അനുവദിച്ചു, പക്ഷേ PipeWire കണക്ഷൻ തുറക്കാനായില്ല"), ("A GStreamer plugin needed for screen capture is missing ({})", "സ്ക്രീൻ പകർത്താൻ ആവശ്യമായ GStreamer പ്ലഗിൻ ലഭ്യമല്ല ({})"),
("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 സെക്കൻഡിനായി ശൂന്യമാക്കിയിടുക."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Aktiver WebRTC P2P-tilkobling"), ("Enable WebRTC P2P connection", "Aktiver WebRTC P2P-tilkobling"),
("Enable TCP hole punching", "Aktiver TCP hole punching"), ("Enable TCP hole punching", "Aktiver TCP hole punching"),
("The screen sharing request was declined on the remote device", "Forespørselen om skjermdeling ble avvist på den eksterne enheten"), ("The screen sharing request was declined on the remote device", "Forespørselen om skjermdeling ble avvist på den eksterne enheten"),
("The screen sharing request timed out on the remote device", "Forespørselen om skjermdeling fikk tidsavbrudd på den eksterne enheten"), ("No one responded to the screen sharing request on the remote device", "Ingen svarte på forespørselen om skjermdeling på den eksterne enheten"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk får ikke tilgang til skrivebordsøkten på den eksterne enheten, kontroller at en økt kjører og at RustDesk kan bruke den"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal avsluttet forespørselen om skjermdeling ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Skrivebordsportalen på den eksterne enheten mangler en funksjon som kreves for skjermdeling eller fjernstyring, bakstykket er kanskje ikke installert"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal returnerte ingen skjerm å ta opp, PipeWire-biblioteket kan være for gammelt"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Skjermdeling ble godkjent på den eksterne enheten, men PipeWire-tilkoblingen kunne ikke åpnes"), ("A GStreamer plugin needed for screen capture is missing ({})", "Et GStreamer-tillegg som kreves for skjermopptak mangler ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P-verbinding inschakelen"), ("Enable WebRTC P2P connection", "WebRTC P2P-verbinding inschakelen"),
("Enable TCP hole punching", "TCP-hole punching inschakelen"), ("Enable TCP hole punching", "TCP-hole punching inschakelen"),
("The screen sharing request was declined on the remote device", "Het verzoek om schermdeling is geweigerd op het externe apparaat"), ("The screen sharing request was declined on the remote device", "Het verzoek om schermdeling is geweigerd op het externe apparaat"),
("The screen sharing request timed out on the remote device", "Het verzoek om schermdeling is verlopen op het externe apparaat"), ("No one responded to the screen sharing request on the remote device", "Niemand heeft gereageerd op het verzoek om schermdeling op het externe apparaat"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk heeft geen toegang tot de bureaubladsessie op het externe apparaat, controleer of er een sessie actief is en of RustDesk die kan gebruiken"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal heeft het verzoek om schermdeling beëindigd ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "De bureaubladportal op het externe apparaat mist een functie die nodig is voor schermdeling of besturing op afstand, de backend is mogelijk niet geïnstalleerd"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal heeft geen scherm teruggegeven om op te nemen, de PipeWire-bibliotheek is mogelijk te oud"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Schermdeling is goedgekeurd op het externe apparaat, maar de PipeWire-verbinding kon niet worden geopend"), ("A GStreamer plugin needed for screen capture is missing ({})", "Een GStreamer-plug-in die nodig is voor schermopname ontbreekt ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Włącz połączenie P2P WebRTC"), ("Enable WebRTC P2P connection", "Włącz połączenie P2P WebRTC"),
("Enable TCP hole punching", "Włącz tworzenie tunelu TCP"), ("Enable TCP hole punching", "Włącz tworzenie tunelu TCP"),
("The screen sharing request was declined on the remote device", "Żądanie udostępnienia ekranu zostało odrzucone na urządzeniu zdalnym"), ("The screen sharing request was declined on the remote device", "Żądanie udostępnienia ekranu zostało odrzucone na urządzeniu zdalnym"),
("The screen sharing request timed out on the remote device", "Upłynął limit czasu żądania udostępnienia ekranu na urządzeniu zdalnym"), ("No one responded to the screen sharing request on the remote device", "Nikt nie odpowiedział na żądanie udostępnienia ekranu na urządzeniu zdalnym"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk nie może uzyskać dostępu do sesji pulpitu na urządzeniu zdalnym, sprawdź, czy sesja działa i czy RustDesk może z niej korzystać"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal zakończył żądanie udostępnienia ekranu ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Portalowi pulpitu na urządzeniu zdalnym brakuje funkcji wymaganej do udostępniania ekranu lub zdalnego sterowania, jego zaplecze może nie być zainstalowane"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal nie zwrócił żadnego ekranu do przechwycenia, biblioteka PipeWire może być zbyt stara"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Udostępnianie ekranu zostało zatwierdzone na urządzeniu zdalnym, ale nie udało się otworzyć połączenia PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "Brak wtyczki GStreamer wymaganej do przechwytywania ekranu ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Ativar ligação P2P por WebRTC"), ("Enable WebRTC P2P connection", "Ativar ligação P2P por WebRTC"),
("Enable TCP hole punching", "Ativar TCP hole punching"), ("Enable TCP hole punching", "Ativar TCP hole punching"),
("The screen sharing request was declined on the remote device", "O pedido de partilha de ecrã foi recusado no dispositivo remoto"), ("The screen sharing request was declined on the remote device", "O pedido de partilha de ecrã foi recusado no dispositivo remoto"),
("The screen sharing request timed out on the remote device", "O pedido de partilha de ecrã expirou no dispositivo remoto"), ("No one responded to the screen sharing request on the remote device", "Ninguém respondeu ao pedido de partilha de ecrã 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 aceder à sessão de ambiente de trabalho no dispositivo remoto, verifique se existe uma sessão ativa e se o RustDesk a pode usar"), ("The XDG Desktop Portal ended the screen sharing request ({})", "O XDG Desktop Portal terminou o pedido de partilha de ecrã ({})"),
("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 ambiente de trabalho do dispositivo remoto uma capacidade necessária para partilha de ecrã ou controlo remoto, o seu backend pode não estar instalado"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "O XDG Desktop Portal não devolveu qualquer ecrã para capturar, a biblioteca PipeWire pode ser demasiado antiga"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "A partilha de ecrã foi aprovada no dispositivo remoto, mas não foi possível abrir a ligação PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "Falta um plugin do GStreamer necessário para capturar o ecrã ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -691,7 +691,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Incorrect username or password.", "Usuário ou senha incorretos"), ("Incorrect username or password.", "Usuário ou senha incorretos"),
("The user is not an administrator.", "O usuário não é administrador"), ("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"), ("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"),
("Supported only in the installed version.", "Suportado somente na versão instalada"), ("Supported only in the installed version.", "Funciona somente na versão instalada"),
("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"), ("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"),
("Preparing for installation ...", "Preparando para instalação ..."), ("Preparing for installation ...", "Preparando para instalação ..."),
("Show my cursor", "Mostrar meu cursor"), ("Show my cursor", "Mostrar meu cursor"),
@@ -767,18 +767,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Allow terminal apps to copy to clipboard", ""), ("Allow terminal apps to copy to clipboard", ""),
("Enable", "Habilitar"), ("Enable", "Habilitar"),
("Reuse one connection for port forwarding", "Reutilizar uma conexão para encaminhamento de portas"), ("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 estabelecer uma nova conexão 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 conectar e fazer login novamente para cada uma."),
("Enable WebRTC P2P connection", "Habilitar conexão WebRTC P2P"), ("Enable WebRTC P2P connection", "Habilitar conexão WebRTC P2P"),
("Enable TCP hole punching", "Habilitar TCP hole punching"), ("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 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"), ("No one responded to the screen sharing request on the remote device", "Ninguém respondeu à solicitação de compartilhamento de tela 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 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 XDG Desktop Portal ended the screen sharing request ({})", "O XDG Desktop Portal encerrou a solicitação de compartilhamento de tela ({})"),
("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."), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "O XDG Desktop Portal não retornou nenhuma tela para capturar, a biblioteca PipeWire pode ser muito antiga"),
("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."), ("A GStreamer plugin needed for screen capture is missing ({})", "Falta um plugin do GStreamer necessário para capturar a tela ({})"),
("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", "Quanto tempo uma conexão de retransmissão já estabelecida espera pela conexão direta WebRTC antes de ser usada no lugar dela. Aumente para dar mais tempo a uma conexão direta lenta; diminua para recorrer mais cedo ao retransmissor em redes onde não é possível uma conexão direta. Deixe vazio para o valor padrão de 2.5 segundos."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Activează conexiunea P2P prin WebRTC"), ("Enable WebRTC P2P connection", "Activează conexiunea P2P prin WebRTC"),
("Enable TCP hole punching", "Activează traversarea TCP (hole punching)"), ("Enable TCP hole punching", "Activează traversarea TCP (hole punching)"),
("The screen sharing request was declined on the remote device", "Cererea de partajare a ecranului a fost refuzată pe dispozitivul de la distanță"), ("The screen sharing request was declined on the remote device", "Cererea de partajare a ecranului a fost refuzată pe dispozitivul de la distanță"),
("The screen sharing request timed out on the remote device", "Cererea de partajare a ecranului a expirat pe dispozitivul de la distanță"), ("No one responded to the screen sharing request on the remote device", "Nimeni nu a răspuns la cererea de partajare a ecranului pe dispozitivul de la distanță"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk nu poate accesa sesiunea de desktop de pe dispozitivul de la distanță, verificați dacă o sesiune rulează și dacă RustDesk o poate folosi"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal a încheiat cererea de partajare a ecranului ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Portalului de desktop de pe dispozitivul de la distanță îi lipsește o funcționalitate necesară pentru partajarea ecranului sau controlul de la distanță, componenta sa de bază poate lipsi"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal nu a returnat niciun ecran de capturat, biblioteca PipeWire poate fi prea veche"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Partajarea ecranului a fost aprobată pe dispozitivul de la distanță, dar conexiunea PipeWire nu a putut fi deschisă"), ("A GStreamer plugin needed for screen capture is missing ({})", "Lipsește un plugin GStreamer necesar pentru capturarea ecranului ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Использовать подключение WebRTC P2P"), ("Enable WebRTC P2P connection", "Использовать подключение WebRTC P2P"),
("Enable TCP hole punching", "Использовать TCP hole punching"), ("Enable TCP hole punching", "Использовать TCP hole punching"),
("The screen sharing request was declined on the remote device", "Запрос на демонстрацию экрана отклонён на удалённом устройстве"), ("The screen sharing request was declined on the remote device", "Запрос на демонстрацию экрана отклонён на удалённом устройстве"),
("The screen sharing request timed out on the remote device", "Истекло время ожидания запроса на демонстрацию экрана на удалённом устройстве"), ("No one responded to the screen sharing request 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"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal завершил запрос на демонстрацию экрана ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Порталу рабочего стола на удалённом устройстве не хватает возможности, необходимой для демонстрации экрана или удалённого управления, его реализация может быть не установлена"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal не вернул экран для захвата, библиотека PipeWire может быть слишком старой"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Демонстрация экрана была разрешена на удалённом устройстве, но не удалось открыть соединение PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "Отсутствует плагин GStreamer, необходимый для захвата экрана ({})"),
("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 секунды."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Abìlita connessione P2P WebRTC"), ("Enable WebRTC P2P connection", "Abìlita connessione P2P WebRTC"),
("Enable TCP hole punching", "Abìlita s'istampadura TCP"), ("Enable TCP hole punching", "Abìlita s'istampadura TCP"),
("The screen sharing request was declined on the remote device", "Sa rechesta de cumpartzidura de sa schermada est istada refudada in su dispositivu remotu"), ("The screen sharing request was declined on the remote device", "Sa rechesta de cumpartzidura de sa schermada est istada refudada in su dispositivu remotu"),
("The screen sharing request timed out on the remote device", "Sa rechesta de cumpartzidura de sa schermada at superadu su tempus in su dispositivu remotu"), ("No one responded to the screen sharing request on the remote device", "Nemos at rispostu a sa rechesta de cumpartzidura de sa schermada in su dispositivu remotu"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk no podet acèdere a sa sessione de iscrivania in su dispositivu remotu, controlla chi una sessione siat ativa e chi RustDesk la potzat impreare"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal at acabadu sa rechesta de cumpartzidura de sa schermada ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "A su portale de iscrivania in su dispositivu remotu li mancat una funtzionalidade netzessària pro sa cumpartzidura de sa schermada o pro su controllu remotu, su backend suo podet non èssere installadu"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal no at torradu peruna schermada de registrare, sa libreria PipeWire podet èssere tropu betza"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Sa cumpartzidura de sa schermada est istada aprovada in su dispositivu remotu, ma no si est pòdidu abèrrere sa connessione PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "Mancat unu plugin de GStreamer netzessàriu pro registrare sa schermada ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Povoliť pripojenie WebRTC P2P"), ("Enable WebRTC P2P connection", "Povoliť pripojenie WebRTC P2P"),
("Enable TCP hole punching", "Povoliť TCP hole punching"), ("Enable TCP hole punching", "Povoliť TCP hole punching"),
("The screen sharing request was declined on the remote device", "Žiadosť o zdieľanie obrazovky bola na vzdialenom zariadení odmietnutá"), ("The screen sharing request was declined on the remote device", "Žiadosť o zdieľanie obrazovky bola na vzdialenom zariadení odmietnutá"),
("The screen sharing request timed out on the remote device", "Vypršal časový limit žiadosti o zdieľanie obrazovky na vzdialenom zariadení"), ("No one responded to the screen sharing request on the remote device", "Na žiadosť o zdieľanie obrazovky na vzdialenom zariadení nikto neodpovedal"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk nemá prístup k relácii plochy na vzdialenom zariadení, overte, či relácia beží a či ju RustDesk môže použiť"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal ukončil žiadosť o zdieľanie obrazovky ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Portálu plochy na vzdialenom zariadení chýba funkcia potrebná na zdieľanie obrazovky alebo vzdialené ovládanie, jeho implementácia možno nie je nainštalovaná"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal nevrátil žiadnu obrazovku na zachytenie, knižnica PipeWire môže byť príliš stará"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Zdieľanie obrazovky bolo na vzdialenom zariadení schválené, ale pripojenie PipeWire sa nepodarilo otvoriť"), ("A GStreamer plugin needed for screen capture is missing ({})", "Chýba zásuvný modul GStreamer potrebný na zachytenie obrazovky ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Omogoči povezavo WebRTC P2P"), ("Enable WebRTC P2P connection", "Omogoči povezavo WebRTC P2P"),
("Enable TCP hole punching", "Omogoči preboj lukenj TCP"), ("Enable TCP hole punching", "Omogoči preboj lukenj TCP"),
("The screen sharing request was declined on the remote device", "Zahteva za skupno rabo zaslona je bila na oddaljeni napravi zavrnjena"), ("The screen sharing request was declined on the remote device", "Zahteva za skupno rabo zaslona je bila na oddaljeni napravi zavrnjena"),
("The screen sharing request timed out on the remote device", "Zahteva za skupno rabo zaslona je na oddaljeni napravi potekla"), ("No one responded to the screen sharing request on the remote device", "Nihče ni odgovoril na zahtevo za skupno rabo zaslona na oddaljeni napravi"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk ne more dostopati do namizne seje na oddaljeni napravi, preverite, ali seja teče in ali jo RustDesk lahko uporablja"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal je končal zahtevo za skupno rabo zaslona ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Namiznemu portalu na oddaljeni napravi manjka zmožnost, potrebna za skupno rabo zaslona ali oddaljeno upravljanje, njegovo zaledje morda ni nameščeno"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal ni vrnil nobenega zaslona za zajem, knjižnica PipeWire je morda prestara"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Skupna raba zaslona je bila na oddaljeni napravi odobrena, vendar povezave PipeWire ni bilo mogoče odpreti"), ("A GStreamer plugin needed for screen capture is missing ({})", "Manjka vtičnik GStreamer, potreben za zajem zaslona ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Aktivizo lidhjen WebRTC P2P"), ("Enable WebRTC P2P connection", "Aktivizo lidhjen WebRTC P2P"),
("Enable TCP hole punching", "Aktivizo TCP hole punching"), ("Enable TCP hole punching", "Aktivizo TCP hole punching"),
("The screen sharing request was declined on the remote device", "Kërkesa për ndarjen e ekranit u refuzua në pajisjen e largët"), ("The screen sharing request was declined on the remote device", "Kërkesa për ndarjen e ekranit u refuzua në pajisjen e largët"),
("The screen sharing request timed out on the remote device", "Kërkesa për ndarjen e ekranit skadoi në pajisjen e largët"), ("No one responded to the screen sharing request on the remote device", "Askush nuk iu përgjigj kërkesës për ndarjen e ekranit në pajisjen e largët"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk nuk mund të arrijë sesionin e desktopit në pajisjen e largët, kontrolloni që një sesion desktopi po funksionon dhe që RustDesk mund ta përdorë"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal e përfundoi kërkesën për ndarjen e ekranit ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Portalit të desktopit në pajisjen e largët i mungon një aftësi e nevojshme për ndarjen e ekranit ose kontrollin në distancë, backend-i i tij mund të mos jetë i instaluar"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal nuk ktheu asnjë ekran për regjistrim, biblioteka PipeWire mund të jetë shumë e vjetër"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Ndarja e ekranit u miratua në pajisjen e largët, por lidhja PipeWire nuk mund të hapej"), ("A GStreamer plugin needed for screen capture is missing ({})", "Mungon një shtojcë e GStreamer e nevojshme për regjistrimin e ekranit ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Omogući WebRTC P2P konekciju"), ("Enable WebRTC P2P connection", "Omogući WebRTC P2P konekciju"),
("Enable TCP hole punching", "Omogući TCP hole punching"), ("Enable TCP hole punching", "Omogući TCP hole punching"),
("The screen sharing request was declined on the remote device", "Zahtev za deljenje ekrana je odbijen na udaljenom uređaju"), ("The screen sharing request was declined on the remote device", "Zahtev za deljenje ekrana je odbijen na udaljenom uređaju"),
("The screen sharing request timed out on the remote device", "Zahtev za deljenje ekrana je istekao na udaljenom uređaju"), ("No one responded to the screen sharing request on the remote device", "Niko nije odgovorio na zahtev za deljenje ekrana na udaljenom uređaju"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk ne može da pristupi sesiji radne površine na udaljenom uređaju, proverite da li sesija radi i da li RustDesk može da je koristi"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal je završio zahtev za deljenje ekrana ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Portalu radne površine na udaljenom uređaju nedostaje mogućnost potrebna za deljenje ekrana ili daljinsko upravljanje, njegov pozadinski deo možda nije instaliran"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal nije vratio nijedan ekran za snimanje, PipeWire biblioteka je možda prestara"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Deljenje ekrana je odobreno na udaljenom uređaju, ali PipeWire vezu nije bilo moguće otvoriti"), ("A GStreamer plugin needed for screen capture is missing ({})", "Nedostaje GStreamer dodatak potreban za snimanje ekrana ({})"),
("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 секунде."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Aktivera WebRTC P2P anslutning"), ("Enable WebRTC P2P connection", "Aktivera WebRTC P2P anslutning"),
("Enable TCP hole punching", "Aktivera TCP hålslagning"), ("Enable TCP hole punching", "Aktivera TCP hålslagning"),
("The screen sharing request was declined on the remote device", "Begäran om skärmdelning avvisades på fjärrenheten"), ("The screen sharing request was declined on the remote device", "Begäran om skärmdelning avvisades på fjärrenheten"),
("The screen sharing request timed out on the remote device", "Begäran om skärmdelning nådde tidsgränsen på fjärrenheten"), ("No one responded to the screen sharing request on the remote device", "Ingen svarade på begäran om skärmdelning på fjärrenheten"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk kan inte nå skrivbordssessionen på fjärrenheten, kontrollera att en session körs och att RustDesk kan använda den"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal avslutade begäran om skärmdelning ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Skrivbordsportalen på fjärrenheten saknar en funktion som krävs för skärmdelning eller fjärrstyrning, dess bakände är kanske inte installerad"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal returnerade ingen skärm att spela in, PipeWire-biblioteket kan vara för gammalt"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Skärmdelning godkändes på fjärrenheten, men PipeWire-anslutningen kunde inte öppnas"), ("A GStreamer plugin needed for screen capture is missing ({})", "En GStreamer-insticksmodul som krävs för skärminspelning saknas ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P இணைப்பு இயக்கு"), ("Enable WebRTC P2P connection", "WebRTC P2P இணைப்பு இயக்கு"),
("Enable TCP hole punching", "TCP hole punching இயக்கு"), ("Enable TCP hole punching", "TCP hole punching இயக்கு"),
("The screen sharing request was declined on the remote device", "தொலைநிலை சாதனத்தில் திரை பகிர்வு கோரிக்கை நிராகரிக்கப்பட்டது"), ("The screen sharing request was declined on the remote device", "தொலைநிலை சாதனத்தில் திரை பகிர்வு கோரிக்கை நிராகரிக்கப்பட்டது"),
("The screen sharing request timed out on the remote device", "தொலைநிலை சாதனத்தில் திரை பகிர்வு கோரிக்கையின் நேரம் முடிந்தது"), ("No one responded to the screen sharing request 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 அதைப் பயன்படுத்த முடியுமா என்பதையும் சரிபார்க்கவும்"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal திரை பகிர்வு கோரிக்கையை முடித்தது ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "தொலைநிலை சாதனத்தின் டெஸ்க்டாப் போர்ட்டலில் திரை பகிர்வுக்கோ தொலை கட்டுப்பாட்டுக்கோ தேவையான திறன் இல்லை, அதன் பின்தளம் நிறுவப்படாமல் இருக்கலாம்"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal பதிவு செய்ய எந்தத் திரையையும் வழங்கவில்லை, PipeWire நூலகம் மிகவும் பழையதாக இருக்கலாம்"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "தொலைநிலை சாதனத்தில் திரை பகிர்வு அனுமதிக்கப்பட்டது, ஆனால் PipeWire இணைப்பைத் திறக்க முடியவில்லை"), ("A GStreamer plugin needed for screen capture is missing ({})", "திரைப் பதிவுக்குத் தேவையான GStreamer செருகுநிரல் இல்லை ({})"),
("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 வினாடிகளுக்கு காலியாக விடவும்."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", ""), ("Enable WebRTC P2P connection", ""),
("Enable TCP hole punching", ""), ("Enable TCP hole punching", ""),
("The screen sharing request was declined on the remote device", ""), ("The screen sharing request was declined on the remote device", ""),
("The screen sharing request timed out on the remote device", ""), ("No one responded to the screen sharing request 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 XDG Desktop Portal ended the screen sharing request ({})", ""),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", ""), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", ""),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", ""), ("A GStreamer plugin needed for screen capture is missing ({})", ""),
("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", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "เปิดใช้งานการเชื่อมต่อ P2P แบบ WebRTC"), ("Enable WebRTC P2P connection", "เปิดใช้งานการเชื่อมต่อ P2P แบบ WebRTC"),
("Enable TCP hole punching", "เปิดใช้งาน TCP hole punching"), ("Enable TCP hole punching", "เปิดใช้งาน TCP hole punching"),
("The screen sharing request was declined on the remote device", "คำขอแชร์หน้าจอถูกปฏิเสธบนอุปกรณ์ระยะไกล"), ("The screen sharing request was declined on the remote device", "คำขอแชร์หน้าจอถูกปฏิเสธบนอุปกรณ์ระยะไกล"),
("The screen sharing request timed out on the remote device", "คำขอแชร์หน้าจอบนอุปกรณ์ระยะไกลหมดเวลา"), ("No one responded to the screen sharing request 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 ใช้งานได้"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal ได้ยุติคำขอแชร์หน้าจอ ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "พอร์ทัลเดสก์ท็อปบนอุปกรณ์ระยะไกลขาดความสามารถที่จำเป็นสำหรับการแชร์หน้าจอหรือการควบคุมระยะไกล แบ็กเอนด์ของมันอาจยังไม่ได้ติดตั้ง"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal ไม่ได้ส่งคืนหน้าจอสำหรับการบันทึก ไลบรารี PipeWire อาจเก่าเกินไป"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "การแชร์หน้าจอได้รับอนุญาตบนอุปกรณ์ระยะไกลแล้ว แต่ไม่สามารถเปิดการเชื่อมต่อ PipeWire ได้"), ("A GStreamer plugin needed for screen capture is missing ({})", "ไม่พบปลั๊กอิน GStreamer ที่จำเป็นสำหรับการบันทึกหน้าจอ ({})"),
("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 วินาที"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P bağlantısını etkinleştir"), ("Enable WebRTC P2P connection", "WebRTC P2P bağlantısını etkinleştir"),
("Enable TCP hole punching", "TCP delik açmayı etkinleştir"), ("Enable TCP hole punching", "TCP delik açmayı etkinleştir"),
("The screen sharing request was declined on the remote device", "Ekran paylaşımı isteği uzak cihazda reddedildi"), ("The screen sharing request was declined on the remote device", "Ekran paylaşımı isteği uzak cihazda reddedildi"),
("The screen sharing request timed out on the remote device", "Uzak cihazdaki ekran paylaşımı isteği zaman aşımına uğradı"), ("No one responded to the screen sharing request on the remote device", "Uzak cihazdaki ekran paylaşımı isteğine kimse yanıt vermedi"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk uzak cihazdaki masaüstü oturumuna erişemiyor, bir masaüstü oturumunun çalıştığını ve RustDesk tarafından kullanılabildiğini doğrulayın"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal ekran paylaşımı isteğini sonlandırdı ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Uzak cihazdaki masaüstü portalında ekran paylaşımı veya uzaktan denetim için gereken bir yetenek yok, arka ucu kurulu olmayabilir"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal yakalanacak ekran döndürmedi, PipeWire kitaplığı çok eski olabilir"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Ekran paylaşımı uzak cihazda onaylandı, ancak PipeWire bağlantısıılamadı"), ("A GStreamer plugin needed for screen capture is missing ({})", "Ekran yakalama için gereken GStreamer eklentisi eksik ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -770,15 +770,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("port-forward-mux-tip", "同一條連接埠轉送規則上的所有連線共用一條到對方的連線,而不是每條連線都重新連線並登入一次。"), ("port-forward-mux-tip", "同一條連接埠轉送規則上的所有連線共用一條到對方的連線,而不是每條連線都重新連線並登入一次。"),
("Enable WebRTC P2P connection", "啟用 WebRTC P2P 連線"), ("Enable WebRTC P2P connection", "啟用 WebRTC P2P 連線"),
("Enable TCP hole punching", "啟用 TCP 打洞"), ("Enable TCP hole punching", "啟用 TCP 打洞"),
("The screen sharing request was declined on the remote device", "遠端裝置上的使用者拒絕了螢幕分享要求"), ("The screen sharing request was declined on the remote device", "遠端裝置上拒絕了螢幕分享要求"),
("The screen sharing request timed out on the remote device", "遠端裝置上螢幕分享要求逾時了"), ("No one responded to the screen sharing request 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 可以使用它"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal 結束了螢幕分享要求 ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "遠端裝置上的桌面入口缺少螢幕分享或遠端控制所需的功能,可能沒有安裝它的後端"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal 未傳回可擷取的螢幕PipeWire 函式庫可能過舊"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "遠端裝置上已核准螢幕分享,但無法開啟 PipeWire 連線"), ("A GStreamer plugin needed for screen capture is missing ({})", "缺少螢幕擷取所需的 GStreamer 外掛程式 ({})"),
("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 秒。"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Увімкнути P2P-підключення через WebRTC"), ("Enable WebRTC P2P connection", "Увімкнути P2P-підключення через WebRTC"),
("Enable TCP hole punching", "Увімкнути TCP hole punching"), ("Enable TCP hole punching", "Увімкнути TCP hole punching"),
("The screen sharing request was declined on the remote device", "Запит на демонстрацію екрана відхилено на віддаленому пристрої"), ("The screen sharing request was declined on the remote device", "Запит на демонстрацію екрана відхилено на віддаленому пристрої"),
("The screen sharing request timed out on the remote device", "Час очікування запиту на демонстрацію екрана на віддаленому пристрої вичерпано"), ("No one responded to the screen sharing request 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 його використовувати"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal завершив запит на демонстрацію екрана ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Порталу стільниці на віддаленому пристрої бракує можливості, потрібної для демонстрації екрана або віддаленого керування, його реалізацію може бути не встановлено"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal не повернув екран для захоплення, бібліотека PipeWire може бути застарою"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Демонстрацію екрана було схвалено на віддаленому пристрої, але не вдалося відкрити з'єднання PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "Відсутній плагін GStreamer, потрібний для захоплення екрана ({})"),
("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 секунди."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,15 +771,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P کنکشن کو فعال کریں"), ("Enable WebRTC P2P connection", "WebRTC P2P کنکشن کو فعال کریں"),
("Enable TCP hole punching", "TCP ہول پنچنگ کو فعال کریں"), ("Enable TCP hole punching", "TCP ہول پنچنگ کو فعال کریں"),
("The screen sharing request was declined on the remote device", "ریموٹ ڈیوائس پر اسکرین شیئرنگ کی درخواست مسترد کر دی گئی"), ("The screen sharing request was declined on the remote device", "ریموٹ ڈیوائس پر اسکرین شیئرنگ کی درخواست مسترد کر دی گئی"),
("The screen sharing request timed out on the remote device", "ریموٹ ڈیوائس پر اسکرین شیئرنگ کی درخواست کا وقت ختم ہو گیا"), ("No one responded to the screen sharing request 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 اسے استعمال کر سکتا ہے"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal نے اسکرین شیئرنگ کی درخواست ختم کر دی ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "ریموٹ ڈیوائس کے ڈیسک ٹاپ پورٹل میں اسکرین شیئرنگ یا ریموٹ کنٹرول کے لیے درکار صلاحیت موجود نہیں، شاید اس کا بیک اینڈ نصب نہیں ہے"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal نے ریکارڈنگ کے لیے کوئی اسکرین واپس نہیں کی، PipeWire لائبریری شاید بہت پرانی ہے"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "ریموٹ ڈیوائس پر اسکرین شیئرنگ کی منظوری مل گئی، لیکن PipeWire کنکشن نہیں کھولا جا سکا"), ("A GStreamer plugin needed for screen capture is missing ({})", "اسکرین ریکارڈنگ کے لیے درکار GStreamer پلگ ان موجود نہیں ہے ({})"),
("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 سیکنڈ کے لیے خالی چھوڑ دیں۔"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -771,14 +771,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Cho phép kết nối WebRTC P2P"), ("Enable WebRTC P2P connection", "Cho phép kết nối WebRTC P2P"),
("Enable TCP hole punching", "Bật TCP Hole Punching"), ("Enable TCP hole punching", "Bật TCP Hole Punching"),
("The screen sharing request was declined on the remote device", "Yêu cầu chia sẻ màn hình đã bị từ chối trên thiết bị từ xa"), ("The screen sharing request was declined on the remote device", "Yêu cầu chia sẻ màn hình đã bị từ chối trên thiết bị từ xa"),
("The screen sharing request timed out on the remote device", "Yêu cầu chia sẻ màn hình đã hết thời gian chờ trên thiết bị từ xa"), ("No one responded to the screen sharing request on the remote device", "Không có ai phản hồi yêu cầu chia sẻ màn hình trên thiết bị từ xa"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk không thể truy cập phiên màn hình nền trên thiết bị từ xa, hãy kiểm tra rằng một phiên đang chạy và RustDesk có thể dùng nó"), ("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal đã kết thúc yêu cầu chia sẻ màn hình ({})"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Cổng màn hình nền trên thiết bị từ xa thiếu một khả năng cần cho chia sẻ màn hình hoặc điều khiển từ xa, phần nền của nó có thể chưa được cài đặt"), ("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal không trả về màn hình nào để ghi, thư viện PipeWire có thể quá cũ"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Chia sẻ màn hình đã được chấp thuận trên thiết bị từ xa, nhưng không thể mở kết nối PipeWire"), ("A GStreamer plugin needed for screen capture is missing ({})", "Thiếu phần bổ trợ GStreamer cần cho việc ghi màn hình ({})"),
("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."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -1,5 +1,3 @@
#[cfg(any(test, not(target_os = "linux")))]
mod audio_resampler;
mod keyboard; mod keyboard;
/// cbindgen:ignore /// cbindgen:ignore
pub mod platform; pub mod platform;

View File

@@ -15,9 +15,7 @@
use super::*; use super::*;
#[cfg(not(any(target_os = "linux", target_os = "android")))] #[cfg(not(any(target_os = "linux", target_os = "android")))]
use hbb_common::anyhow::anyhow; use hbb_common::anyhow::anyhow;
#[cfg(any(target_os = "linux", target_os = "android"))] use magnum_opus::{Application::*, Channels::*, Encoder};
use magnum_opus::Application::LowDelay;
use magnum_opus::{Channels::*, Encoder};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
pub const NAME: &'static str = "audio"; pub const NAME: &'static str = "audio";
@@ -99,11 +97,10 @@ mod pa_impl {
RESTARTING.store(false, Ordering::SeqCst); RESTARTING.store(false, Ordering::SeqCst);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let mut stream = crate::ipc::connect(1000, "_pa").await?; let mut stream = crate::ipc::connect(1000, "_pa").await?;
let mut encoder = AudioEncoder::new(Encoder::new( unsafe {
crate::platform::PA_SAMPLE_RATE, AUDIO_ZERO_COUNT = 0;
Stereo, }
LowDelay, let mut encoder = Encoder::new(crate::platform::PA_SAMPLE_RATE, Stereo, LowDelay)?;
)?);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
allow_err!( allow_err!(
stream stream
@@ -174,29 +171,20 @@ pub fn is_screen_capture_kit_available() -> bool {
.any(|host| *host == cpal::HostId::ScreenCaptureKit) .any(|host| *host == cpal::HostId::ScreenCaptureKit)
} }
#[cfg(not(any(target_os = "linux", target_os = "android")))]
mod audio_capture;
#[cfg(not(any(target_os = "linux", target_os = "android")))]
mod audio_capture_error;
#[cfg(not(any(target_os = "linux", target_os = "android")))]
mod audio_capture_queue;
#[cfg(not(any(target_os = "linux", target_os = "android")))] #[cfg(not(any(target_os = "linux", target_os = "android")))]
mod cpal_impl { mod cpal_impl {
use self::service::{Reset, ServiceSwap}; use self::service::{Reset, ServiceSwap};
use super::audio_capture_error::CaptureErrorHandler;
use super::*; use super::*;
use cpal::{ use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait}, traits::{DeviceTrait, HostTrait, StreamTrait},
Device, Host, InputCallbackInfo, SupportedStreamConfig, BufferSize, Device, Host, InputCallbackInfo, StreamConfig, SupportedStreamConfig,
}; };
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref HOST: Host = cpal::default_host(); static ref HOST: Host = cpal::default_host();
static ref INPUT_BUFFER: Arc<Mutex<std::collections::VecDeque<f32>>> = Default::default();
} }
const AUDIO_PACKETS_PER_SECOND: usize = 100;
#[cfg(feature = "screencapturekit")] #[cfg(feature = "screencapturekit")]
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref HOST_SCREEN_CAPTURE_KIT: Result<Host, cpal::HostUnavailable> = cpal::host_from_id(cpal::HostId::ScreenCaptureKit); static ref HOST_SCREEN_CAPTURE_KIT: Result<Host, cpal::HostUnavailable> = cpal::host_from_id(cpal::HostId::ScreenCaptureKit);
@@ -204,20 +192,7 @@ mod cpal_impl {
#[derive(Default)] #[derive(Default)]
pub struct State { pub struct State {
stream: Option<ActiveCaptureStream>, stream: Option<(Box<dyn StreamTrait>, Arc<Message>)>,
}
struct ActiveCaptureStream {
stream: Option<Box<dyn StreamTrait>>,
format: Arc<Message>,
_encoder_worker: audio_capture_queue::CaptureEncoderWorker,
errors: CaptureErrorHandler,
}
impl Drop for ActiveCaptureStream {
fn drop(&mut self) {
self.stream.take();
}
} }
impl super::service::Reset for State { impl super::service::Reset for State {
@@ -235,10 +210,8 @@ mod cpal_impl {
} }
_ => {} _ => {}
} }
if let Some(stream) = &state.stream { if let Some((_, format)) = &state.stream {
sp.send_shared(stream.format.clone()); sp.send_shared(format.clone());
#[cfg(target_os = "macos")]
log::info!("Audio capture stream recreated; replacement format sent");
} }
RESTARTING.store(false, Ordering::SeqCst); RESTARTING.store(false, Ordering::SeqCst);
Ok(()) Ok(())
@@ -252,8 +225,8 @@ mod cpal_impl {
} }
_ => {} _ => {}
} }
if let Some(stream) = &state.stream { if let Some((_, format)) = &state.stream {
sps.send_shared(stream.format.clone()); sps.send_shared(format.clone());
} }
Ok(()) Ok(())
})?; })?;
@@ -261,13 +234,6 @@ mod cpal_impl {
} }
pub fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { pub fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> {
if let Some(stream) = &state.stream {
if stream.errors.needs_restart() {
// Recreate on the service thread, outside the capture callbacks.
log::warn!("Recreating audio capture stream after an error");
super::restart();
}
}
if !RESTARTING.load(Ordering::SeqCst) { if !RESTARTING.load(Ordering::SeqCst) {
run_serv_snapshot(sp, state) run_serv_snapshot(sp, state)
} else { } else {
@@ -275,89 +241,29 @@ mod cpal_impl {
} }
} }
#[derive(Clone, Copy)] fn send(
struct CaptureFrameProcessorConfig { data: Vec<f32>,
input_rate: u32, sample_rate0: u32,
output_rate: u32, sample_rate: u32,
device_channel: u16, device_channel: u16,
encode_channel: u16, encode_channel: u16,
} encoder: &mut Encoder,
sp: &GenericService,
struct CaptureFrameProcessor { ) {
config: CaptureFrameProcessorConfig, let mut data = data;
resampler: Option<crate::audio_resampler::FixedFrameAudioResampler>, if sample_rate0 != sample_rate {
sender: audio_capture_queue::CapturePcmSender, data = crate::common::audio_resample(&data, sample_rate0, sample_rate, device_channel);
rechannel_buffer: Vec<f32>,
}
struct CaptureStreamOutput {
sender: audio_capture_queue::CapturePcmSender,
sample_rate: u32,
encode_channel: magnum_opus::Channels,
}
impl CaptureFrameProcessor {
fn new(
config: CaptureFrameProcessorConfig,
sender: audio_capture_queue::CapturePcmSender,
) -> ResultType<Self> {
let resampler = if config.input_rate == config.output_rate {
None
} else {
let output_frames = config.output_rate as usize / AUDIO_PACKETS_PER_SECOND;
Some(crate::audio_resampler::FixedFrameAudioResampler::new(
crate::audio_resampler::AudioResamplerConfig {
input_rate: config.input_rate,
output_rate: config.output_rate,
channels: config.device_channel,
},
output_frames,
)?)
};
Ok(Self {
config,
resampler,
sender,
rechannel_buffer: Vec::with_capacity(
capture_packet_layout(config.output_rate, config.encode_channel)?.1,
),
})
} }
if device_channel != encode_channel {
fn process(&mut self, data: &[f32]) -> ResultType<()> { data = crate::common::audio_rechannel(
let config = self.config; data,
let sender = &mut self.sender; sample_rate,
let rechannel_buffer = &mut self.rechannel_buffer; sample_rate,
let mut send_packet = |packet: &[f32]| { device_channel,
let packet = encode_channel,
audio_capture::rechannel(packet, config.device_channel, rechannel_buffer);
sender.submit(packet);
};
if let Some(resampler) = self.resampler.as_mut() {
resampler.process_with(data, send_packet).with_context(|| {
format!(
"Failed to resample captured audio from {} Hz to {} Hz",
config.input_rate, config.output_rate
)
})?;
} else {
send_packet(data);
}
Ok(())
}
}
fn capture_packet_layout(sample_rate: u32, channels: u16) -> ResultType<(usize, usize)> {
if sample_rate < AUDIO_PACKETS_PER_SECOND as u32 || channels == 0 {
bail!("Invalid audio capture layout: sample_rate={sample_rate}, channels={channels}");
}
let frames = sample_rate as usize / AUDIO_PACKETS_PER_SECOND;
let samples = frames.checked_mul(channels as usize).with_context(|| {
format!(
"Audio capture frame size overflow: sample_rate={sample_rate}, channels={channels}"
) )
})?; }
Ok((frames, samples)) send_f32(&data, encoder, sp);
} }
#[cfg(feature = "screencapturekit")] #[cfg(feature = "screencapturekit")]
@@ -447,7 +353,7 @@ mod cpal_impl {
Ok((device, format)) Ok((device, format))
} }
fn play(sp: &GenericService) -> ResultType<ActiveCaptureStream> { fn play(sp: &GenericService) -> ResultType<(Box<dyn StreamTrait>, Arc<Message>)> {
use cpal::SampleFormat::*; use cpal::SampleFormat::*;
let (device, config) = get_device()?; let (device, config) = get_device()?;
let sp = sp.clone(); let sp = sp.clone();
@@ -465,273 +371,85 @@ mod cpal_impl {
48000 48000
}; };
let ch = if config.channels() > 1 { Stereo } else { Mono }; let ch = if config.channels() > 1 { Stereo } else { Mono };
let max_channels = config.channels().max(ch as u16); let stream = match config.sample_format() {
let (_, max_packet_samples) = capture_packet_layout(sample_rate, max_channels)?; I8 => build_input_stream::<i8>(device, &config, sp, sample_rate, ch)?,
let encoder_config = audio_capture_queue::CaptureEncoderConfig { I16 => build_input_stream::<i16>(device, &config, sp, sample_rate, ch)?,
sample_rate, I32 => build_input_stream::<i32>(device, &config, sp, sample_rate, ch)?,
encode_channel: ch, I64 => build_input_stream::<i64>(device, &config, sp, sample_rate, ch)?,
max_packet_samples, U8 => build_input_stream::<u8>(device, &config, sp, sample_rate, ch)?,
}; U16 => build_input_stream::<u16>(device, &config, sp, sample_rate, ch)?,
let (sender, encoder_worker) = U32 => build_input_stream::<u32>(device, &config, sp, sample_rate, ch)?,
audio_capture_queue::start_capture_encoder(encoder_config, sp)?; U64 => build_input_stream::<u64>(device, &config, sp, sample_rate, ch)?,
let output = CaptureStreamOutput { F32 => build_input_stream::<f32>(device, &config, sp, sample_rate, ch)?,
sender, F64 => build_input_stream::<f64>(device, &config, sp, sample_rate, ch)?,
sample_rate,
encode_channel: ch,
};
let (stream, errors) = match config.sample_format() {
I8 => build_input_stream::<i8>(device, &config, output)?,
I16 => build_input_stream::<i16>(device, &config, output)?,
I32 => build_input_stream::<i32>(device, &config, output)?,
I64 => build_input_stream::<i64>(device, &config, output)?,
U8 => build_input_stream::<u8>(device, &config, output)?,
U16 => build_input_stream::<u16>(device, &config, output)?,
U32 => build_input_stream::<u32>(device, &config, output)?,
U64 => build_input_stream::<u64>(device, &config, output)?,
F32 => build_input_stream::<f32>(device, &config, output)?,
F64 => build_input_stream::<f64>(device, &config, output)?,
f => bail!("unsupported audio format: {:?}", f), f => bail!("unsupported audio format: {:?}", f),
}; };
stream.play()?; stream.play()?;
#[cfg(target_os = "macos")] Ok((
log::info!("Audio capture start call succeeded"); Box::new(stream),
Ok(ActiveCaptureStream { Arc::new(create_format_msg(sample_rate, ch as _)),
stream: Some(Box::new(stream)), ))
format: Arc::new(create_format_msg(sample_rate, ch as _)),
_encoder_worker: encoder_worker,
errors,
})
}
fn convert_input_samples<T>(data: &[T]) -> impl Iterator<Item = f32> + '_
where
T: cpal::SizedSample,
f32: cpal::FromSample<T>,
{
data.iter()
.map(|sample| <f32 as cpal::FromSample<T>>::from_sample_(*sample))
}
#[cfg(target_os = "macos")]
fn log_capture_startup<T>(
data: &[T],
received_samples: bool,
received_signal: bool,
) -> (bool, bool)
where
T: cpal::SizedSample,
f32: cpal::FromSample<T>,
{
// Starting capture does not guarantee sample delivery or audible data.
if !received_samples && !data.is_empty() {
log::info!(
"Audio capture received first PCM block: {} samples",
data.len()
);
}
let has_signal = received_signal
|| convert_input_samples(data).any(|sample| sample.is_finite() && sample != 0.0);
if !received_signal && has_signal {
log::info!("Audio capture received first nonzero PCM");
}
(received_samples || !data.is_empty(), has_signal)
} }
fn build_input_stream<T>( fn build_input_stream<T>(
device: cpal::Device, device: cpal::Device,
config: &cpal::SupportedStreamConfig, config: &cpal::SupportedStreamConfig,
output: CaptureStreamOutput, sp: GenericService,
) -> ResultType<(cpal::Stream, CaptureErrorHandler)> sample_rate: u32,
encode_channel: magnum_opus::Channels,
) -> ResultType<cpal::Stream>
where where
T: cpal::SizedSample, T: cpal::SizedSample + dasp::sample::ToSample<f32>,
f32: cpal::FromSample<T>,
{ {
let errors = CaptureErrorHandler::default(); let err_fn = move |err| {
let callback_errors = errors.clone(); // too many UnknownErrno, will improve later
let err_fn = move |err| callback_errors.handle(err); log::trace!("an error occurred on stream: {}", err);
let processor_errors = errors.clone();
#[cfg(target_os = "macos")]
let (mut received_samples, mut received_signal) = (false, false);
let sample_rate_0 = config.sample_rate().0;
log::debug!("Audio sample rate : {}", output.sample_rate);
let device_channel = config.channels();
let (_, capture_frame_samples) = capture_packet_layout(sample_rate_0, device_channel)?;
let mut frame = audio_capture::CaptureFrameBuffer::new(capture_frame_samples)?;
let processor_config = CaptureFrameProcessorConfig {
input_rate: sample_rate_0,
output_rate: output.sample_rate,
device_channel,
encode_channel: output.encode_channel as _,
}; };
let mut processor = CaptureFrameProcessor::new(processor_config, output.sender)?; let sample_rate_0 = config.sample_rate().0;
log::debug!("Audio sample rate : {}", sample_rate);
unsafe {
AUDIO_ZERO_COUNT = 0;
}
let device_channel = config.channels();
let mut encoder = Encoder::new(sample_rate, encode_channel, LowDelay)?;
// https://www.opus-codec.org/docs/html_api/group__opusencoder.html#gace941e4ef26ed844879fde342ffbe546
// https://chromium.googlesource.com/chromium/deps/opus/+/1.1.1/include/opus.h
// Do not set `frame_size = sample_rate as usize / 100;`
// Because we find `sample_rate as usize / 100` will cause encoder error in `encoder.encode_vec_float()` sometimes.
// https://github.com/xiph/opus/blob/2554a89e02c7fc30a980b4f7e635ceae1ecba5d6/src/opus_encoder.c#L725
let frame_size = sample_rate_0 as usize / 100; // 10 ms
let encode_len = frame_size * encode_channel as usize;
let rechannel_len = encode_len * device_channel as usize / encode_channel as usize;
INPUT_BUFFER.lock().unwrap().clear();
let timeout = None; let timeout = None;
let stream_config = StreamConfig {
channels: device_channel,
sample_rate: config.sample_rate(),
buffer_size: BufferSize::Default,
};
let stream = device.build_input_stream( let stream = device.build_input_stream(
&config.config(), &stream_config,
move |data: &[T], _: &InputCallbackInfo| { move |data: &[T], _: &InputCallbackInfo| {
if processor_errors.needs_restart() { let buffer: Vec<f32> = data.iter().map(|s| T::to_sample(*s)).collect();
return; let mut lock = INPUT_BUFFER.lock().unwrap();
lock.extend(buffer);
while lock.len() >= rechannel_len {
let frame: Vec<f32> = lock.drain(0..rechannel_len).collect();
send(
frame,
sample_rate_0,
sample_rate,
device_channel,
encode_channel as _,
&mut encoder,
&sp,
);
} }
#[cfg(target_os = "macos")]
{
(received_samples, received_signal) =
log_capture_startup(data, received_samples, received_signal);
}
frame.process(convert_input_samples(data), |frame| {
processor_errors.process_frame(|| processor.process(frame));
});
}, },
err_fn, err_fn,
timeout, timeout,
)?; )?;
Ok((stream, errors)) Ok(stream)
}
#[cfg(test)]
mod tests {
use super::super::audio_capture_queue::{
new_pcm_handoff, start_capture_encoder, CaptureEncoderConfig,
};
use super::{
capture_packet_layout, convert_input_samples, CaptureFrameProcessor,
CaptureFrameProcessorConfig,
};
use crate::audio_resampler::allocation_tests::assert_no_allocations;
use crate::server::EmptyExtraFieldService;
use magnum_opus::Channels::{Mono, Stereo};
const INVALID_CAPTURE_RATE: u32 = 99;
const RATE_24_KHZ: u32 = 24_000;
const RATE_44_1_KHZ: u32 = 44_100;
const RATE_48_KHZ: u32 = 48_000;
const MONO_CHANNELS: u16 = 1;
const NEGATIVE_FULL_SCALE_LIMIT: f32 = -0.99;
const POSITIVE_FULL_SCALE_LIMIT: f32 = 0.99;
const STEREO_CHANNELS: u16 = 2;
const SURROUND_CHANNELS: u16 = 6;
const ZERO_CHANNELS: u16 = 0;
#[test]
fn capture_sample_conversion_uses_cpal_traits() {
let input = [i16::MIN, 0, i16::MAX];
let output: Vec<_> = convert_input_samples(&input).collect();
assert_eq!(output.len(), input.len());
assert!(output[0] <= NEGATIVE_FULL_SCALE_LIMIT);
assert_eq!(output[1], 0.0);
assert!(output[2] >= POSITIVE_FULL_SCALE_LIMIT);
}
#[test]
fn capture_packet_layout_validates_rate_and_channels() {
let expected_frames = RATE_48_KHZ as usize / super::AUDIO_PACKETS_PER_SECOND;
assert_eq!(
capture_packet_layout(RATE_48_KHZ, STEREO_CHANNELS).unwrap(),
(expected_frames, expected_frames * STEREO_CHANNELS as usize)
);
assert!(capture_packet_layout(INVALID_CAPTURE_RATE, MONO_CHANNELS).is_err());
assert!(capture_packet_layout(RATE_48_KHZ, ZERO_CHANNELS).is_err());
}
#[test]
fn capture_callback_pipeline_does_not_allocate_after_warmup() {
for (input_rate, output_rate, device_channel, encode_channel) in [
(RATE_48_KHZ, RATE_48_KHZ, MONO_CHANNELS, MONO_CHANNELS),
(RATE_48_KHZ, RATE_48_KHZ, STEREO_CHANNELS, STEREO_CHANNELS),
(RATE_44_1_KHZ, RATE_24_KHZ, STEREO_CHANNELS, STEREO_CHANNELS),
(RATE_48_KHZ, RATE_48_KHZ, SURROUND_CHANNELS, STEREO_CHANNELS),
] {
assert_capture_processor_does_not_allocate(CaptureFrameProcessorConfig {
input_rate,
output_rate,
device_channel,
encode_channel,
});
}
}
#[test]
fn capture_pcm_handoff_reuses_buffers_and_accounts_for_loss() {
const QUEUE_CAPACITY: usize = 2;
const PACKET_SAMPLES: usize = 4;
const FIRST: [f32; PACKET_SAMPLES] = [1.0; PACKET_SAMPLES];
const SECOND: [f32; PACKET_SAMPLES] = [2.0; PACKET_SAMPLES];
const THIRD: [f32; PACKET_SAMPLES] = [3.0; PACKET_SAMPLES];
const OVERSIZED_SAMPLES: usize = PACKET_SAMPLES + 1;
const OVERSIZED: [f32; OVERSIZED_SAMPLES] = [1.0; OVERSIZED_SAMPLES];
let (mut sender, receiver) = new_pcm_handoff(QUEUE_CAPACITY, PACKET_SAMPLES).unwrap();
sender.set_wake_thread(std::thread::current()).unwrap();
assert_no_allocations(|| {
sender.submit(&FIRST);
sender.submit(&SECOND);
sender.submit(&THIRD);
});
let loss = receiver.take_loss();
assert_eq!(loss.dropped, 1);
assert_eq!(loss.oversized, 0);
assert_eq!(loss.recycle_failures, 0);
let second = receiver.pop().unwrap();
let third = receiver.pop().unwrap();
assert_eq!(second, SECOND);
assert_eq!(third, THIRD);
receiver.recycle(second);
receiver.recycle(third);
assert!(receiver.is_empty());
assert_no_allocations(|| sender.submit(&OVERSIZED));
let loss = receiver.take_loss();
assert_eq!(loss.dropped, 0);
assert_eq!(loss.oversized, 1);
assert_eq!(loss.recycle_failures, 0);
assert!(receiver.is_empty());
}
#[test]
fn capture_pcm_handoff_rejects_invalid_layouts() {
assert!(new_pcm_handoff(0, 1).is_err());
assert!(new_pcm_handoff(1, 0).is_err());
}
fn assert_capture_processor_does_not_allocate(config: CaptureFrameProcessorConfig) {
const INPUT_LEVEL: f32 = 0.25;
const TEST_SERVICE_NAME: &str = "audio-allocation-test";
let service = EmptyExtraFieldService::new(TEST_SERVICE_NAME.to_owned(), true).sp;
let encode_channel = if config.encode_channel == MONO_CHANNELS {
Mono
} else {
Stereo
};
let encoder_config = CaptureEncoderConfig {
sample_rate: config.output_rate,
encode_channel,
max_packet_samples: config.output_rate as usize / super::AUDIO_PACKETS_PER_SECOND
* config.device_channel.max(config.encode_channel) as usize,
};
let (sender, worker) = start_capture_encoder(encoder_config, service).unwrap();
let mut processor = CaptureFrameProcessor::new(config, sender).unwrap();
let errors = super::CaptureErrorHandler::default();
let input = vec![
INPUT_LEVEL;
config.input_rate as usize / super::AUDIO_PACKETS_PER_SECOND
* config.device_channel as usize
];
let mut frame_buffer =
super::audio_capture::CaptureFrameBuffer::new(input.len()).unwrap();
frame_buffer.process(convert_input_samples(&input), |frame| {
errors.process_frame(|| processor.process(frame));
});
assert_no_allocations(|| {
frame_buffer.process(convert_input_samples(&input), |frame| {
errors.process_frame(|| processor.process(frame));
});
});
assert!(!errors.needs_restart());
drop(processor);
drop(worker);
}
} }
} }
@@ -748,43 +466,28 @@ fn create_format_msg(sample_rate: u32, channels: u16) -> Message {
msg msg
} }
// Use a per-encoder counter for the Noise(Zero) Gate Attack Time. // use AUDIO_ZERO_COUNT for the Noise(Zero) Gate Attack Time
// every audio data length is set to 480 // every audio data length is set to 480
// MAX_AUDIO_ZERO_COUNT=800 is similar as Gate Attack Time 3~5s(Linux) || 6~8s(Windows) // MAX_AUDIO_ZERO_COUNT=800 is similar as Gate Attack Time 3~5s(Linux) || 6~8s(Windows)
const MAX_AUDIO_ZERO_COUNT: u16 = 800; const MAX_AUDIO_ZERO_COUNT: u16 = 800;
static mut AUDIO_ZERO_COUNT: u16 = 0;
struct AudioEncoder { fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) {
encoder: Encoder, if data.iter().filter(|x| **x != 0.).next().is_some() {
zero_count: u16, unsafe {
} AUDIO_ZERO_COUNT = 0;
impl AudioEncoder {
fn new(encoder: Encoder) -> Self {
Self {
encoder,
zero_count: 0,
} }
} } else {
unsafe {
fn should_encode(&mut self, data: &[f32]) -> bool { if AUDIO_ZERO_COUNT > MAX_AUDIO_ZERO_COUNT {
if data.iter().filter(|x| **x != 0.).next().is_some() { if AUDIO_ZERO_COUNT == MAX_AUDIO_ZERO_COUNT + 1 {
self.zero_count = 0; log::debug!("Audio Zero Gate Attack");
} else if self.zero_count > MAX_AUDIO_ZERO_COUNT { AUDIO_ZERO_COUNT += 1;
if self.zero_count == MAX_AUDIO_ZERO_COUNT + 1 { }
log::debug!("Audio Zero Gate Attack"); return;
self.zero_count += 1;
} }
return false; AUDIO_ZERO_COUNT += 1;
} else {
self.zero_count += 1;
} }
true
}
}
fn send_f32(data: &[f32], encoder: &mut AudioEncoder, sp: &GenericService) {
if !encoder.should_encode(data) {
return;
} }
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ {
@@ -797,7 +500,6 @@ fn send_f32(data: &[f32], encoder: &mut AudioEncoder, sp: &GenericService) {
let n = input_size / BATCH_SIZE; let n = input_size / BATCH_SIZE;
for i in 0..n { for i in 0..n {
match encoder match encoder
.encoder
.encode_vec_float(&data[i * BATCH_SIZE..(i + 1) * BATCH_SIZE], BATCH_SIZE) .encode_vec_float(&data[i * BATCH_SIZE..(i + 1) * BATCH_SIZE], BATCH_SIZE)
{ {
Ok(data) => { Ok(data) => {
@@ -808,7 +510,7 @@ fn send_f32(data: &[f32], encoder: &mut AudioEncoder, sp: &GenericService) {
}); });
sp.send(msg_out); sp.send(msg_out);
} }
Err(error) => log::warn!("Failed to encode audio frame: {error:?}"), Err(_) => {}
} }
} }
} else { } else {
@@ -818,7 +520,7 @@ fn send_f32(data: &[f32], encoder: &mut AudioEncoder, sp: &GenericService) {
} }
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
match encoder.encoder.encode_vec_float(data, data.len() * 6) { match encoder.encode_vec_float(data, data.len() * 6) {
Ok(data) => { Ok(data) => {
let mut msg_out = Message::new(); let mut msg_out = Message::new();
msg_out.set_audio_frame(AudioFrame { msg_out.set_audio_frame(AudioFrame {
@@ -827,6 +529,6 @@ fn send_f32(data: &[f32], encoder: &mut AudioEncoder, sp: &GenericService) {
}); });
sp.send(msg_out); sp.send(msg_out);
} }
Err(error) => log::warn!("Failed to encode audio frame: {error:?}"), Err(_) => {}
} }
} }

View File

@@ -1,144 +0,0 @@
use hbb_common::anyhow::{bail, Result};
const STEREO_CHANNELS: usize = 2;
pub(super) struct CaptureFrameBuffer {
samples: Vec<f32>,
filled: usize,
}
impl CaptureFrameBuffer {
pub(super) fn new(samples: usize) -> Result<Self> {
if samples == 0 {
bail!("Audio capture frame must contain at least one sample");
}
Ok(Self {
samples: vec![0.0; samples],
filled: 0,
})
}
pub(super) fn process(
&mut self,
input: impl Iterator<Item = f32>,
mut on_frame: impl FnMut(&[f32]),
) {
for sample in input {
self.samples[self.filled] = sample;
self.filled += 1;
if self.filled == self.samples.len() {
self.filled = 0;
on_frame(&self.samples);
}
}
}
}
pub(super) fn rechannel<'a>(
input: &'a [f32],
channels: u16,
output: &'a mut Vec<f32>,
) -> &'a [f32] {
let input = if channels > STEREO_CHANNELS as u16 {
&input[..input.len() / channels as usize * channels as usize]
} else {
input
};
output.clear();
match channels {
3 => rechannel_frame::<3>(input, output),
4 => rechannel_frame::<4>(input, output),
5 => rechannel_frame::<5>(input, output),
6 => rechannel_frame::<6>(input, output),
7 => rechannel_frame::<7>(input, output),
8 => rechannel_frame::<8>(input, output),
// Preserve the existing passthrough for mono/stereo and unsupported layouts.
_ => return input,
}
output
}
fn rechannel_frame<const CHANNELS: usize>(input: &[f32], output: &mut Vec<f32>) {
use fon::{
chan::{Ch32, Channel},
Frame,
};
for samples in input.chunks_exact(CHANNELS) {
let mut frame = Frame::<Ch32, CHANNELS>::default();
for (channel, sample) in frame.channels_mut().iter_mut().zip(samples) {
*channel = (*sample).into();
}
// Match the same-rate Stream::pipe conversion before fon's SinkTo conversion.
let stereo = frame.to::<Ch32, CHANNELS>().to::<Ch32, STEREO_CHANNELS>();
output.extend(stereo.channels().iter().map(|channel| channel.to_f32()));
}
}
#[cfg(test)]
mod tests {
use super::{rechannel, CaptureFrameBuffer};
use crate::audio_resampler::allocation_tests::assert_no_allocations;
#[test]
fn capture_channel_conversion_reuses_storage_and_preserves_mapping() {
const SAMPLE_RATE: u32 = 48_000;
const FRAMES: usize = 7;
const STEREO_CHANNELS: u16 = 2;
const MIN_SAMPLE: f32 = -1.25;
const SAMPLE_STEP: f32 = 0.2;
for channels in [1, 2, 3, 4, 5, 6, 7, 8, 16] {
let input: Vec<_> = (0..FRAMES * channels as usize + 1)
.map(|sample| MIN_SAMPLE + sample as f32 * SAMPLE_STEP)
.collect();
let expected = crate::common::audio_rechannel(
input.clone(),
SAMPLE_RATE,
SAMPLE_RATE,
channels,
channels.min(STEREO_CHANNELS),
);
let mut output = Vec::with_capacity(FRAMES * STEREO_CHANNELS as usize);
assert_no_allocations(|| {
let actual = rechannel(&input, channels, &mut output);
assert_eq!(
actual, expected,
"channel mapping changed for {channels} channels"
);
});
}
}
#[test]
fn capture_framing_retains_partial_input_without_allocating() {
const FRAME_SAMPLES: usize = 6;
const CALLBACK_SIZES: [usize; 7] = [0, 1, 17, 2, 257, 492, 5];
let input: Vec<_> = (0..CALLBACK_SIZES.iter().sum::<usize>())
.map(|sample| sample as f32)
.collect();
let mut buffer = CaptureFrameBuffer::new(FRAME_SAMPLES).unwrap();
let mut input_position = 0;
let mut output_position = 0;
assert!(CaptureFrameBuffer::new(0).is_err());
assert_no_allocations(|| {
for samples in CALLBACK_SIZES {
let end = input_position + samples;
buffer.process(input[input_position..end].iter().copied(), |frame| {
assert_eq!(
frame,
&input[output_position..output_position + FRAME_SAMPLES]
);
output_position += FRAME_SAMPLES;
});
input_position = end;
assert_eq!(
output_position,
input_position / FRAME_SAMPLES * FRAME_SAMPLES
);
}
});
assert_eq!(output_position, input.len());
}
}

View File

@@ -1,128 +0,0 @@
use super::super::AudioEncoder;
use super::{
send_f32, CaptureEncoderConfig, CaptureEncoderContext, CapturePcmReceiver, CapturePcmStats,
CAPTURE_PCM_QUEUE_PACKETS,
};
use hbb_common::log;
use magnum_opus::Channels;
use std::{
sync::atomic::Ordering,
time::{Duration, Instant},
};
const CAPTURE_DECLICK_MS: usize = 5;
const CAPTURE_PACKET_MS: usize = 10;
const MILLISECONDS_PER_SECOND: usize = 1_000;
const MAX_ENCODE_CHANNELS: usize = Channels::Stereo as usize;
const CAPTURE_STATS_LOG_INTERVAL: Duration = Duration::from_secs(5);
struct CaptureEncoderState {
channels: usize,
expected_sequence: usize,
fade_frames: usize,
last_frame: [f32; MAX_ENCODE_CHANNELS],
reporter: CaptureStatsReporter,
}
impl CaptureEncoderState {
fn new(sample_rate: u32, channels: Channels) -> Self {
// The encoder has already validated the supported Opus rate and channel count.
let fade_frames = sample_rate as usize * CAPTURE_DECLICK_MS / MILLISECONDS_PER_SECOND;
Self {
channels: channels as usize,
expected_sequence: 0,
fade_frames,
last_frame: [0.0; MAX_ENCODE_CHANNELS],
reporter: CaptureStatsReporter::new(),
}
}
fn next_packet(&mut self, receiver: &CapturePcmReceiver) -> Option<Vec<f32>> {
self.reporter.pending.add(receiver.take_stats());
self.reporter.report(false);
let (sequence, mut packet) = receiver.pop_packet()?;
self.smooth_packet(sequence, &mut packet);
Some(packet)
}
fn smooth_packet(&mut self, sequence: usize, packet: &mut [f32]) {
if sequence != self.expected_sequence {
// Capture packets contain 10 ms of PCM, so the transition fits in this packet.
for (index, frame) in packet
.chunks_exact_mut(self.channels)
.take(self.fade_frames)
.enumerate()
{
let weight = (index + 1) as f32 / self.fade_frames as f32;
for (channel, sample) in frame.iter_mut().enumerate() {
*sample = self.last_frame[channel] * (1.0 - weight) + *sample * weight;
}
}
}
self.expected_sequence = sequence.wrapping_add(1);
if let Some(frame) = packet.chunks_exact(self.channels).next_back() {
self.last_frame[..self.channels].copy_from_slice(frame);
}
}
}
struct CaptureStatsReporter {
pending: CapturePcmStats,
last_report: Instant,
}
impl CaptureStatsReporter {
fn new() -> Self {
Self {
pending: Default::default(),
last_report: Instant::now(),
}
}
fn report(&mut self, force: bool) {
if self.pending.is_empty() {
return;
}
if !force && self.last_report.elapsed() < CAPTURE_STATS_LOG_INTERVAL {
return;
}
let stats = std::mem::take(&mut self.pending);
log::debug!(
"Audio capture PCM handoff stats: observed_max_queued_packets={}, approx_queued_audio_ms={}, capacity_packets={}",
stats.max_queued_packets,
stats.max_queued_packets.saturating_mul(CAPTURE_PACKET_MS),
CAPTURE_PCM_QUEUE_PACKETS
);
if !stats.loss.is_empty() {
log::warn!(
"Audio capture PCM handoff loss: dropped={}, contention_dropped={}, oversized={}, recycle_failures={}",
stats.loss.dropped,
stats.loss.contention_dropped,
stats.loss.oversized,
stats.loss.recycle_failures
);
}
self.last_report = Instant::now();
}
}
pub(super) fn run_capture_encoder(context: CaptureEncoderContext, config: CaptureEncoderConfig) {
let mut encoder = AudioEncoder::new(context.encoder);
let mut state = CaptureEncoderState::new(config.sample_rate, config.encode_channel);
loop {
while let Some(packet) = state.next_packet(&context.receiver) {
send_f32(&packet, &mut encoder, &context.service);
context.receiver.recycle(packet);
}
if context.stop.load(Ordering::Acquire) && context.receiver.is_empty() {
state.reporter.pending.add(context.receiver.take_stats());
state.reporter.report(true);
return;
}
std::thread::park_timeout(CAPTURE_STATS_LOG_INTERVAL);
}
}
#[cfg(test)]
#[path = "audio_capture_encoder_tests.rs"]
mod tests;

View File

@@ -1,186 +0,0 @@
use super::super::{new_pcm_handoff, CAPTURE_PCM_QUEUE_PACKETS};
use super::*;
use crate::audio_resampler::allocation_tests::assert_no_allocations;
use magnum_opus::{Application::LowDelay, Decoder, Encoder};
const SAMPLE_RATE: u32 = 48_000;
const PACKETS_PER_SECOND: usize = 100;
const PACKET_COUNT: usize = 120;
const PAUSE_PACKET: usize = 50;
const DROPPED_PACKETS: usize = 6;
const SIGNAL_FREQUENCY: f64 = 97.0;
const SIGNAL_AMPLITUDE: f64 = 0.5;
const ACTIVE_LEVEL: f32 = 0.8;
const SAMPLE_TOLERANCE: f32 = 0.000001;
const MAX_ENCODE_BYTES_PER_SAMPLE: usize = 6;
fn signal_packet(index: usize, channels: usize) -> Vec<f32> {
let frames = SAMPLE_RATE as usize / PACKETS_PER_SECOND;
(0..frames)
.flat_map(|frame| {
let phase = std::f64::consts::TAU * SIGNAL_FREQUENCY * (index * frames + frame) as f64
/ f64::from(SAMPLE_RATE);
(0..channels)
.map(move |channel| (SIGNAL_AMPLITUDE * (phase + channel as f64).sin()) as f32)
})
.collect()
}
fn encoded_audio(dropped: usize, channels: Channels) -> Vec<f32> {
let samples = SAMPLE_RATE as usize / PACKETS_PER_SECOND * channels as usize;
let (mut sender, receiver) = new_pcm_handoff(CAPTURE_PCM_QUEUE_PACKETS, samples).unwrap();
let mut state = CaptureEncoderState::new(SAMPLE_RATE, channels);
let mut encoder = Encoder::new(SAMPLE_RATE, channels, LowDelay).unwrap();
let mut decoder = Decoder::new(SAMPLE_RATE, channels).unwrap();
let mut decoded = vec![0.0; samples];
let mut output = Vec::new();
let resume_packet = PAUSE_PACKET + CAPTURE_PCM_QUEUE_PACKETS + dropped - 1;
for index in 0..PACKET_COUNT {
sender.submit(&signal_packet(index, channels as usize));
if (PAUSE_PACKET..resume_packet).contains(&index) {
continue;
}
if index == resume_packet {
let stats = receiver.take_stats();
assert_eq!(stats.max_queued_packets, CAPTURE_PCM_QUEUE_PACKETS);
assert_eq!(stats.loss.dropped, dropped);
assert_eq!(stats.loss.contention_dropped, 0);
assert_eq!(stats.loss.oversized, 0);
assert_eq!(stats.loss.recycle_failures, 0);
}
while let Some(packet) = state.next_packet(&receiver) {
let encoded = encoder
.encode_vec_float(&packet, samples * MAX_ENCODE_BYTES_PER_SAMPLE)
.unwrap();
let frames = decoder.decode_float(&encoded, &mut decoded, false).unwrap();
assert_eq!(frames * channels as usize, samples);
output.extend_from_slice(&decoded);
receiver.recycle(packet);
}
}
assert_eq!(output.len(), (PACKET_COUNT - dropped) * samples);
output
}
fn maximum_join_step(pcm: &[f32], channels: usize) -> f32 {
let samples = SAMPLE_RATE as usize / PACKETS_PER_SECOND * channels;
pcm[(PAUSE_PACKET - 1) * samples..(PAUSE_PACKET + 2) * samples]
.windows(channels + 1)
.map(|window| (window[channels] - window[0]).abs())
.fold(0.0, f32::max)
}
#[test]
fn capture_overflow_is_smoothed_before_encoding() {
const MAX_STEP_RATIO: f32 = 2.0;
for channels in [Channels::Mono, Channels::Stereo] {
let clean = encoded_audio(0, channels);
let overflow = encoded_audio(DROPPED_PACKETS, channels);
let clean_step = maximum_join_step(&clean, channels as usize);
let overflow_step = maximum_join_step(&overflow, channels as usize);
assert!(clean_step > 0.0);
assert!(
overflow_step < clean_step * MAX_STEP_RATIO,
"capture discard introduced a sharp join: clean={clean_step}, overflow={overflow_step}"
);
}
}
#[test]
fn rejected_packets_mark_the_gap_after_already_queued_audio() {
const CAPACITY: usize = 3;
let samples = SAMPLE_RATE as usize / PACKETS_PER_SECOND;
let (mut sender, receiver) = new_pcm_handoff(CAPACITY, samples).unwrap();
let mut state = CaptureEncoderState::new(SAMPLE_RATE, Channels::Mono);
let active = vec![ACTIVE_LEVEL; samples];
let opposite = vec![-ACTIVE_LEVEL; samples];
sender.submit(&active);
let packet = state.next_packet(&receiver).unwrap();
assert_eq!(packet, active);
receiver.recycle(packet);
sender.submit(&active);
sender.submit(&active);
sender.submit(&vec![ACTIVE_LEVEL; samples + 1]);
sender.submit(&opposite);
assert_eq!(receiver.take_loss().oversized, 1);
assert_no_allocations(|| {
for _ in 0..CAPACITY - 1 {
let packet = state.next_packet(&receiver).unwrap();
assert_eq!(packet, active);
receiver.recycle(packet);
}
let packet = state.next_packet(&receiver).unwrap();
let expected = ACTIVE_LEVEL * (1.0 - 2.0 / state.fade_frames as f32);
assert!((packet[0] - expected).abs() < SAMPLE_TOLERANCE);
assert_eq!(&packet[samples / 2..], &opposite[samples / 2..]);
receiver.recycle(packet);
});
}
#[test]
fn gaps_and_sequence_wrap_preserve_channel_history() {
const RATE_8_KHZ: u32 = 8_000;
for (rate, channels) in [
(RATE_8_KHZ, Channels::Mono),
(RATE_8_KHZ, Channels::Stereo),
(SAMPLE_RATE, Channels::Mono),
(SAMPLE_RATE, Channels::Stereo),
] {
let channels_count = channels as usize;
let mut state = CaptureEncoderState::new(rate, channels);
let samples = rate as usize / PACKETS_PER_SECOND * channels_count;
let active: Vec<_> = [ACTIVE_LEVEL, -ACTIVE_LEVEL][..channels_count]
.iter()
.copied()
.cycle()
.take(samples)
.collect();
let mut first = active.clone();
state.smooth_packet(0, &mut first);
assert_eq!(first, active);
let mut opposite: Vec<_> = active.iter().map(|sample| -sample).collect();
let mut resumed = active.clone();
assert_no_allocations(|| {
state.smooth_packet(2, &mut opposite);
state.smooth_packet(4, &mut resumed);
});
for channel in 0..channels_count {
let expected = active[channel] * (1.0 - 2.0 / state.fade_frames as f32);
assert!((opposite[channel] - expected).abs() < SAMPLE_TOLERANCE);
let previous = opposite[samples - channels_count + channel];
let expected = previous + (active[channel] - previous) / state.fade_frames as f32;
assert!((resumed[channel] - expected).abs() < SAMPLE_TOLERANCE);
}
assert_eq!(&resumed[samples / 2..], &active[samples / 2..]);
state.expected_sequence = usize::MAX;
let mut last = active.clone();
let mut wrapped: Vec<_> = active.iter().map(|sample| -sample).collect();
let expected = wrapped.clone();
assert_no_allocations(|| {
state.smooth_packet(usize::MAX, &mut last);
state.smooth_packet(0, &mut wrapped);
});
assert_eq!(last, active);
assert_eq!(wrapped, expected);
}
}
#[test]
fn capture_loss_is_reported_while_packets_remain_queued() {
const CAPACITY: usize = 2;
let samples = SAMPLE_RATE as usize / PACKETS_PER_SECOND;
let (mut sender, receiver) = new_pcm_handoff(CAPACITY, samples).unwrap();
let mut state = CaptureEncoderState::new(SAMPLE_RATE, Channels::Mono);
let before_report = Instant::now() - CAPTURE_STATS_LOG_INTERVAL;
state.reporter.last_report = before_report;
let packet = vec![ACTIVE_LEVEL; samples];
for _ in 0..=CAPACITY {
sender.submit(&packet);
}
let packet = state.next_packet(&receiver).unwrap();
assert!(!receiver.is_empty());
assert!(state.reporter.last_report > before_report);
assert!(state.reporter.pending.is_empty());
assert!(receiver.take_loss().is_empty());
receiver.recycle(packet);
}

View File

@@ -1,129 +0,0 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
// Each stream owns its flag so a late callback cannot restart a replacement.
#[derive(Clone, Default)]
pub(super) struct CaptureErrorHandler {
interrupted: Arc<AtomicBool>,
}
impl CaptureErrorHandler {
pub(super) fn handle(&self, error: cpal::StreamError) {
if matches!(
error,
cpal::StreamError::StreamInterrupted { .. } | cpal::StreamError::DeviceNotAvailable
) {
// ScreenCaptureKit can stop capture while the remote session stays open.
// The observed -3821 error does not identify its underlying trigger.
// https://developer.apple.com/documentation/screencapturekit/scstreamdelegate/stream(_:didstopwitherror:)
hbb_common::log::error!("Audio capture stream interrupted: {error}");
self.interrupted.store(true, Ordering::Relaxed);
} else {
// Keep frequent sample-buffer errors at the existing trace level.
hbb_common::log::trace!("an error occurred on stream: {error}");
}
}
pub(super) fn process_frame(&self, process: impl FnOnce() -> hbb_common::ResultType<()>) {
// Defensive recovery: persistent processing failures with valid capture input
// have not been reproduced. Stop using a failed processor until stream replacement.
if self.needs_restart() {
return;
}
if let Err(error) = process() {
self.interrupted.store(true, Ordering::Relaxed);
hbb_common::log::error!(
"Failed to process captured audio frame; requesting stream restart: {error:#}"
);
}
}
pub(super) fn needs_restart(&self) -> bool {
self.interrupted.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use cpal::{BackendSpecificError, StreamError};
fn system_interruption() -> StreamError {
StreamError::StreamInterrupted {
err: BackendSpecificError {
description: "ScreenCaptureKit system-stopped capture (-3821)".to_owned(),
},
}
}
#[test]
fn system_interruption_requests_recreation_of_the_active_stream() {
let errors = CaptureErrorHandler::default();
let callback = errors.clone();
assert!(!errors.needs_restart());
callback.handle(system_interruption());
assert!(errors.needs_restart());
}
#[test]
fn late_error_from_an_old_stream_does_not_restart_its_replacement() {
let old_callback = CaptureErrorHandler::default();
let replacement = CaptureErrorHandler::default();
old_callback.handle(system_interruption());
assert!(old_callback.needs_restart());
assert!(!replacement.needs_restart());
}
#[test]
fn other_backend_errors_do_not_request_recreation() {
let errors = CaptureErrorHandler::default();
errors.handle(StreamError::BackendSpecific {
err: BackendSpecificError {
description: "A sample buffer could not be read".to_owned(),
},
});
assert!(!errors.needs_restart());
}
#[test]
fn processing_failure_skips_remaining_frames_until_stream_replacement() {
const FRAME_SAMPLES: usize = 2;
const FRAMES_PER_CALLBACK: usize = 3;
const CALLBACK_COUNT: usize = 2;
const FAILURE_CALL: usize = 2;
let errors = CaptureErrorHandler::default();
let callback_errors = errors.clone();
let mut framer =
super::super::audio_capture::CaptureFrameBuffer::new(FRAME_SAMPLES).unwrap();
let input = [0.0; FRAME_SAMPLES * FRAMES_PER_CALLBACK];
let mut processed = 0;
let mut failures = 0;
// Inject an error to test recovery; this is not a valid-input backend failure reproduction.
for _ in 0..CALLBACK_COUNT {
framer.process(input.iter().copied(), |_| {
callback_errors.process_frame(|| {
processed += 1;
if processed >= FAILURE_CALL {
failures += 1;
hbb_common::anyhow::bail!("Injected capture processing failure");
}
Ok(())
});
});
}
assert_eq!(processed, FAILURE_CALL);
assert_eq!(failures, 1);
assert!(errors.needs_restart());
let replacement = CaptureErrorHandler::default();
replacement.process_frame(|| {
processed += 1;
Ok(())
});
assert_eq!(processed, FAILURE_CALL + 1);
assert!(!replacement.needs_restart());
}
}

View File

@@ -1,289 +0,0 @@
use super::{send_f32, GenericService};
use hbb_common::{
anyhow::{bail, Context, Result},
log,
};
use magnum_opus::{Application::LowDelay, Channels, Encoder};
use std::{
collections::VecDeque,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc, Mutex, OnceLock, TryLockError,
},
thread::{JoinHandle, Thread},
};
#[path = "audio_capture_encoder.rs"]
mod encoder;
const CAPTURE_PCM_QUEUE_PACKETS: usize = 10;
const CAPTURE_ENCODER_THREAD_NAME: &str = "audio-encoder";
#[derive(Debug, Default, PartialEq, Eq)]
pub(super) struct CapturePcmLoss {
pub(super) dropped: usize,
pub(super) contention_dropped: usize,
pub(super) oversized: usize,
pub(super) recycle_failures: usize,
}
impl CapturePcmLoss {
pub(super) fn is_empty(&self) -> bool {
self.dropped == 0 && self.oversized == 0 && self.recycle_failures == 0
}
pub(super) fn add(&mut self, other: Self) {
self.dropped += other.dropped;
self.contention_dropped += other.contention_dropped;
self.oversized += other.oversized;
self.recycle_failures += other.recycle_failures;
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub(super) struct CapturePcmStats {
pub(super) loss: CapturePcmLoss,
pub(super) max_queued_packets: usize,
}
impl CapturePcmStats {
pub(super) fn is_empty(&self) -> bool {
self.loss.is_empty() && self.max_queued_packets == 0
}
pub(super) fn add(&mut self, other: Self) {
self.loss.add(other.loss);
self.max_queued_packets = self.max_queued_packets.max(other.max_queued_packets);
}
}
struct CapturePcmHandoff {
buffers: Mutex<CapturePcmBuffers>,
wake_thread: OnceLock<Thread>,
other_dropped: AtomicUsize,
contention_dropped: AtomicUsize,
oversized: AtomicUsize,
recycle_failures: AtomicUsize,
max_queued_packets: AtomicUsize,
max_samples: usize,
}
struct CapturePcmBuffers {
available: Vec<Vec<f32>>,
ready: VecDeque<(usize, Vec<f32>)>,
}
pub(super) struct CapturePcmSender {
handoff: Arc<CapturePcmHandoff>,
sequence: usize,
}
pub(super) struct CapturePcmReceiver {
handoff: Arc<CapturePcmHandoff>,
}
pub(super) struct CaptureEncoderConfig {
pub(super) sample_rate: u32,
pub(super) encode_channel: Channels,
pub(super) max_packet_samples: usize,
}
pub(super) struct CaptureEncoderWorker {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
impl Drop for CaptureEncoderWorker {
fn drop(&mut self) {
self.stop.store(true, Ordering::Release);
if let Some(handle) = self.handle.take() {
handle.thread().unpark();
if let Err(error) = handle.join() {
log::error!("Failed to join audio encoder thread: {error:?}");
}
}
}
}
struct CaptureEncoderContext {
receiver: CapturePcmReceiver,
encoder: Encoder,
service: GenericService,
stop: Arc<AtomicBool>,
}
pub(super) fn new_pcm_handoff(
capacity: usize,
max_samples: usize,
) -> Result<(CapturePcmSender, CapturePcmReceiver)> {
if capacity == 0 || max_samples == 0 {
bail!("Audio capture PCM handoff requires nonzero capacity and packet size");
}
let handoff = Arc::new(CapturePcmHandoff {
buffers: Mutex::new(CapturePcmBuffers {
available: Vec::with_capacity(capacity),
ready: VecDeque::with_capacity(capacity),
}),
wake_thread: OnceLock::new(),
other_dropped: AtomicUsize::new(0),
contention_dropped: AtomicUsize::new(0),
oversized: AtomicUsize::new(0),
recycle_failures: AtomicUsize::new(0),
max_queued_packets: AtomicUsize::new(0),
max_samples,
});
{
// Initialize the mutex on this thread, including on platforms with lazy allocation.
let mut buffers = handoff.buffers.lock().unwrap();
for _ in 0..capacity {
buffers.available.push(Vec::with_capacity(max_samples));
}
}
Ok((
CapturePcmSender {
handoff: handoff.clone(),
sequence: 0,
},
CapturePcmReceiver { handoff },
))
}
impl CapturePcmSender {
pub(super) fn set_wake_thread(&self, thread: Thread) -> Result<()> {
if self.handoff.wake_thread.set(thread).is_err() {
bail!("Audio capture PCM wake thread is already configured");
}
Ok(())
}
pub(super) fn submit(&mut self, input: &[f32]) {
let sequence = self.sequence;
self.sequence = self.sequence.wrapping_add(1);
if input.len() > self.handoff.max_samples {
self.handoff.oversized.fetch_add(1, Ordering::Relaxed);
self.wake();
return;
}
// Do not wait for a descheduled worker. Contention rejects the current
// packet even if buffers are available; this is separate from drop-oldest
// when the buffer pool is exhausted.
let mut buffers = match self.handoff.buffers.try_lock() {
Ok(buffers) => buffers,
Err(TryLockError::WouldBlock) => {
self.handoff
.contention_dropped
.fetch_add(1, Ordering::Relaxed);
self.wake();
return;
}
Err(TryLockError::Poisoned(error)) => {
self.handoff.other_dropped.fetch_add(1, Ordering::Relaxed);
log::error!("Audio capture PCM handoff is poisoned: {error}");
self.wake();
return;
}
};
if let Some(mut buffer) = self.take_buffer(&mut buffers) {
buffer.clear();
buffer.extend_from_slice(input);
buffers.ready.push_back((sequence, buffer));
self.handoff
.max_queued_packets
.fetch_max(buffers.ready.len(), Ordering::Relaxed);
} else {
self.handoff.other_dropped.fetch_add(1, Ordering::Relaxed);
}
drop(buffers);
self.wake();
}
fn take_buffer(&self, buffers: &mut CapturePcmBuffers) -> Option<Vec<f32>> {
buffers.available.pop().or_else(|| {
let buffer = buffers.ready.pop_front().map(|(_, buffer)| buffer);
if buffer.is_some() {
self.handoff.other_dropped.fetch_add(1, Ordering::Relaxed);
}
buffer
})
}
fn wake(&self) {
if let Some(thread) = self.handoff.wake_thread.get() {
thread.unpark();
}
}
}
impl CapturePcmReceiver {
#[cfg(test)]
pub(super) fn pop(&self) -> Option<Vec<f32>> {
self.pop_packet().map(|(_, buffer)| buffer)
}
fn pop_packet(&self) -> Option<(usize, Vec<f32>)> {
self.handoff.buffers.lock().unwrap().ready.pop_front()
}
pub(super) fn recycle(&self, mut buffer: Vec<f32>) {
buffer.clear();
let mut buffers = self.handoff.buffers.lock().unwrap();
if buffers.available.len() == buffers.available.capacity() {
self.handoff
.recycle_failures
.fetch_add(1, Ordering::Relaxed);
} else {
buffers.available.push(buffer);
}
}
pub(super) fn is_empty(&self) -> bool {
self.handoff.buffers.lock().unwrap().ready.is_empty()
}
pub(super) fn take_loss(&self) -> CapturePcmLoss {
let contention_dropped = self.handoff.contention_dropped.swap(0, Ordering::Relaxed);
CapturePcmLoss {
dropped: self.handoff.other_dropped.swap(0, Ordering::Relaxed) + contention_dropped,
contention_dropped,
oversized: self.handoff.oversized.swap(0, Ordering::Relaxed),
recycle_failures: self.handoff.recycle_failures.swap(0, Ordering::Relaxed),
}
}
pub(super) fn take_stats(&self) -> CapturePcmStats {
CapturePcmStats {
loss: self.take_loss(),
max_queued_packets: self.handoff.max_queued_packets.swap(0, Ordering::Relaxed),
}
}
}
pub(super) fn start_capture_encoder(
config: CaptureEncoderConfig,
service: GenericService,
) -> Result<(CapturePcmSender, CaptureEncoderWorker)> {
let (sender, receiver) = new_pcm_handoff(CAPTURE_PCM_QUEUE_PACKETS, config.max_packet_samples)?;
let encoder = Encoder::new(config.sample_rate, config.encode_channel, LowDelay)?;
let stop = Arc::new(AtomicBool::new(false));
let context = CaptureEncoderContext {
receiver,
encoder,
service,
stop: stop.clone(),
};
let handle = std::thread::Builder::new()
.name(CAPTURE_ENCODER_THREAD_NAME.to_owned())
.spawn(move || encoder::run_capture_encoder(context, config))
.with_context(|| "Failed to start audio encoder thread")?;
let wake_thread = handle.thread().clone();
let worker = CaptureEncoderWorker {
stop,
handle: Some(handle),
};
sender.set_wake_thread(wake_thread)?;
Ok((sender, worker))
}
#[cfg(test)]
#[path = "audio_capture_queue_tests.rs"]
mod tests;

View File

@@ -1,169 +0,0 @@
use super::*;
use crate::audio_resampler::allocation_tests::assert_no_allocations;
use std::{sync::mpsc, time::Duration};
const PACKET_SAMPLES: usize = 4;
const TEST_TIMEOUT: Duration = Duration::from_secs(2);
const CONCURRENT_PACKETS: usize = 10_000;
#[derive(Clone, Copy)]
enum PausePoint {
Recycle,
Consume,
}
struct WorkerPause {
entered: mpsc::Sender<()>,
resume: mpsc::Receiver<()>,
}
fn paused_worker(
receiver: CapturePcmReceiver,
point: PausePoint,
pause: WorkerPause,
) -> CapturePcmReceiver {
let recycled = match point {
PausePoint::Recycle => Some(receiver.pop_packet().unwrap().1),
PausePoint::Consume => None,
};
let consumed = {
let mut buffers = receiver.handoff.buffers.lock().unwrap();
let consumed = match recycled {
Some(mut packet) => {
packet.clear();
buffers.available.push(packet);
None
}
None => Some(buffers.ready.pop_front().unwrap().1),
};
pause.entered.send(()).unwrap();
pause.resume.recv().unwrap();
consumed
};
if let Some(packet) = consumed {
receiver.recycle(packet);
}
receiver
}
fn assert_callback_progress(point: PausePoint) {
let (mut sender, receiver) =
new_pcm_handoff(CAPTURE_PCM_QUEUE_PACKETS, PACKET_SAMPLES).unwrap();
for sequence in 0..CAPTURE_PCM_QUEUE_PACKETS {
sender.submit(&[sequence as f32; PACKET_SAMPLES]);
}
let (entered_tx, entered_rx) = mpsc::channel();
let (resume_tx, resume_rx) = mpsc::channel();
let pause = WorkerPause {
entered: entered_tx,
resume: resume_rx,
};
let worker = std::thread::spawn(move || paused_worker(receiver, point, pause));
sender.set_wake_thread(worker.thread().clone()).unwrap();
entered_rx.recv_timeout(TEST_TIMEOUT).unwrap();
let (completed_tx, completed_rx) = mpsc::channel();
let callback = std::thread::spawn(move || {
let packet = [CAPTURE_PCM_QUEUE_PACKETS as f32; PACKET_SAMPLES];
assert_no_allocations(|| sender.submit(&packet));
completed_tx.send(()).unwrap();
sender
});
let completed = completed_rx.recv_timeout(TEST_TIMEOUT);
// Release and join both threads before asserting progress, including on failure.
resume_tx.send(()).unwrap();
let receiver = worker.join().unwrap();
let mut sender = callback.join().unwrap();
assert!(completed.is_ok(), "callback waited for the encoder worker");
assert_eq!(
receiver.take_loss(),
CapturePcmLoss {
dropped: 1,
contention_dropped: 1,
..Default::default()
}
);
assert_packets_after_contention(&mut sender, &receiver);
}
fn assert_packets_after_contention(sender: &mut CapturePcmSender, receiver: &CapturePcmReceiver) {
let next_sequence = CAPTURE_PCM_QUEUE_PACKETS + 1;
sender.submit(&[next_sequence as f32; PACKET_SAMPLES]);
for expected in (1..CAPTURE_PCM_QUEUE_PACKETS).chain(std::iter::once(next_sequence)) {
let (sequence, packet) = receiver.pop_packet().unwrap();
assert_eq!(sequence, expected);
assert_eq!(packet, [expected as f32; PACKET_SAMPLES]);
receiver.recycle(packet);
}
assert!(receiver.is_empty());
assert!(receiver.take_loss().is_empty());
assert_pool_restored(receiver, CAPTURE_PCM_QUEUE_PACKETS);
}
fn assert_pool_restored(receiver: &CapturePcmReceiver, capacity: usize) {
let buffers = receiver.handoff.buffers.lock().unwrap();
assert_eq!(buffers.available.len(), capacity);
assert!(buffers
.available
.iter()
.all(|buffer| buffer.capacity() >= PACKET_SAMPLES));
}
#[test]
fn callback_finishes_while_worker_recycles_a_buffer() {
assert_callback_progress(PausePoint::Recycle);
}
#[test]
fn callback_finishes_while_worker_releases_a_ready_packet() {
assert_callback_progress(PausePoint::Consume);
}
fn consume_concurrently(
receiver: CapturePcmReceiver,
finished: Arc<AtomicBool>,
first_sequence: usize,
) -> (CapturePcmReceiver, usize) {
let mut received = 0;
let mut next_ordinal = 0;
loop {
if let Some((sequence, packet)) = receiver.pop_packet() {
let ordinal = sequence.wrapping_sub(first_sequence);
assert!(ordinal >= next_ordinal && ordinal < CONCURRENT_PACKETS);
assert_eq!(packet, [ordinal as f32; PACKET_SAMPLES]);
next_ordinal = ordinal + 1;
received += 1;
receiver.recycle(packet);
} else if finished.load(Ordering::Acquire) && receiver.is_empty() {
return (receiver, received);
} else {
std::thread::yield_now();
}
}
}
#[test]
fn concurrent_handoff_preserves_order_buffers_and_loss_counts_across_sequence_wrap() {
const FIRST_SEQUENCE: usize = usize::MAX - CONCURRENT_PACKETS / 2;
for capacity in [1, 2, CAPTURE_PCM_QUEUE_PACKETS] {
let (mut sender, receiver) = new_pcm_handoff(capacity, PACKET_SAMPLES).unwrap();
sender.sequence = FIRST_SEQUENCE;
let finished = Arc::new(AtomicBool::new(false));
let worker_finished = finished.clone();
let worker = std::thread::spawn(move || {
consume_concurrently(receiver, worker_finished, FIRST_SEQUENCE)
});
assert_no_allocations(|| {
for ordinal in 0..CONCURRENT_PACKETS {
sender.submit(&[ordinal as f32; PACKET_SAMPLES]);
}
});
finished.store(true, Ordering::Release);
let (receiver, received) = worker.join().unwrap();
let stats = receiver.take_stats();
assert_eq!(received + stats.loss.dropped, CONCURRENT_PACKETS);
assert_eq!(stats.loss.oversized, 0);
assert_eq!(stats.loss.recycle_failures, 0);
assert!(stats.max_queued_packets <= capacity);
assert_pool_restored(&receiver, capacity);
}
}

View File

@@ -299,12 +299,6 @@ pub struct Connection {
tx_input: std_mpsc::Sender<MessageInput>, tx_input: std_mpsc::Sender<MessageInput>,
// handle input messages // handle input messages
video_ack_required: bool, video_ack_required: bool,
// Diagnostics only, gated by `RUSTDESK_QOS_VERBOSE`: how long the shared
// write path blocked this second. The video send is inline in the message
// loop, so a slow write also delays the delay probe and its reply.
video_send_max_ms: u32,
video_send_sum_ms: u32,
video_send_count: u32,
server_audit_conn: String, server_audit_conn: String,
server_audit_file: String, server_audit_file: String,
controlled_context: Option<ControlledContext>, controlled_context: Option<ControlledContext>,
@@ -510,9 +504,6 @@ impl Connection {
show_my_cursor: false, show_my_cursor: false,
tx_input, tx_input,
video_ack_required: false, video_ack_required: false,
video_send_max_ms: 0,
video_send_sum_ms: 0,
video_send_count: 0,
server_audit_conn: "".to_owned(), server_audit_conn: "".to_owned(),
server_audit_file: "".to_owned(), server_audit_file: "".to_owned(),
controlled_context, controlled_context,
@@ -959,17 +950,10 @@ impl Connection {
video_service::notify_video_frame_fetched(vf.display as usize, id, Some(instant.into())); video_service::notify_video_frame_fetched(vf.display as usize, id, Some(instant.into()));
} }
} }
let send_begin = video_service::qos_diag_verbose().then(Instant::now);
if let Err(err) = conn.stream.send(&value as &Message).await { if let Err(err) = conn.stream.send(&value as &Message).await {
conn.on_close(&err.to_string(), false).await; conn.on_close(&err.to_string(), false).await;
break; break;
} }
if let Some(begin) = send_begin {
let blocked = begin.elapsed().as_millis() as u32;
conn.video_send_max_ms = conn.video_send_max_ms.max(blocked);
conn.video_send_sum_ms = conn.video_send_sum_ms.saturating_add(blocked);
conn.video_send_count += 1;
}
}, },
Some((instant, value)) = rx.recv() => { Some((instant, value)) = rx.recv() => {
let latency = instant.elapsed().as_millis() as i64; let latency = instant.elapsed().as_millis() as i64;
@@ -1056,21 +1040,6 @@ impl Connection {
break; break;
} }
} }
if video_service::qos_diag_verbose() && conn.video_send_count > 0 {
// Joined with `qos_trace` on `t`: a probe that waits behind a
// blocked write is not a slow network.
log::debug!(
"qos_send t={} id={id} frames={} send_max={} send_sum={} queued={}",
hbb_common::get_time(),
conn.video_send_count,
conn.video_send_max_ms,
conn.video_send_sum_ms,
rx_video.len()
);
conn.video_send_max_ms = 0;
conn.video_send_sum_ms = 0;
conn.video_send_count = 0;
}
conn.file_remove_log_control.on_timer().drain(..).map(|x| conn.send_to_cm(x)).count(); conn.file_remove_log_control.on_timer().drain(..).map(|x| conn.send_to_cm(x)).count();
#[cfg(feature = "hwcodec")] #[cfg(feature = "hwcodec")]
conn.update_supported_encoding(); conn.update_supported_encoding();
@@ -5128,11 +5097,7 @@ impl Connection {
// But it's not necessary now and we have to consider two audio services(client, server). // But it's not necessary now and we have to consider two audio services(client, server).
crate::audio_service::set_voice_call_input_device(None, true); crate::audio_service::set_voice_call_input_device(None, true);
log::info!("#{} Connection closed: {}", self.inner.id(), reason); log::info!("#{} Connection closed: {}", self.inner.id(), reason);
if lock if lock && self.lock_after_session_end && self.keyboard {
&& self.lock_after_session_end
&& self.keyboard
&& !raii::AuthedConnID::session_reconnected(self.inner.id(), &self.session_key())
{
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
lock_screen().await; lock_screen().await;
} }
@@ -6693,21 +6658,6 @@ mod raii {
pub struct AuthedConnID(i32, AuthConnType); pub struct AuthedConnID(i32, AuthConnType);
impl AuthedConnID { impl AuthedConnID {
pub(super) fn is_newer_session_remote(c: &AuthedConn, id: i32, key: &SessionKey) -> bool {
c.conn_id > id && c.conn_type == AuthConnType::Remote && &c.session_key == key
}
/// Whether a newer remote control connection of this session has replaced this one. A
/// controlling peer whose link dies reconnects while the connection it left behind runs
/// on here until its own timeout; locking for that one would lock a session that has
/// already resumed on its replacement.
pub fn session_reconnected(id: i32, key: &SessionKey) -> bool {
let conns = AUTHED_CONNS.lock().unwrap();
conns
.iter()
.any(|c| Self::is_newer_session_remote(c, id, key))
}
pub fn new( pub fn new(
conn_id: i32, conn_id: i32,
conn_type: AuthConnType, conn_type: AuthConnType,
@@ -7597,38 +7547,4 @@ mod test {
Ok(BoolOption::NotSet) Ok(BoolOption::NotSet)
); );
} }
#[test]
fn only_a_newer_remote_control_of_the_same_session_keeps_the_screen_unlocked() {
let replaced_by = super::raii::AuthedConnID::is_newer_session_remote;
let key = |session_id, peer: &str| SessionKey {
peer_id: peer.to_owned(),
name: "".to_owned(),
session_id,
};
let conn = |conn_id, conn_type, session_key| AuthedConn {
conn_id,
conn_type,
session_key,
sender: mpsc::unbounded_channel().0,
printer: false,
};
let mine = key(7, "peer");
let remote = AuthConnType::Remote;
assert!(replaced_by(&conn(3, remote, mine.clone()), 2, &mine));
// An older one, and itself: of connections ending at once only the last still locks.
assert!(!replaced_by(&conn(1, remote, mine.clone()), 2, &mine));
assert!(!replaced_by(&conn(2, remote, mine.clone()), 2, &mine));
// A kind that keeps no screen in use.
assert!(!replaced_by(
&conn(3, AuthConnType::Terminal, mine.clone()),
2,
&mine
));
// Another session of this peer, and another peer on the same session id: `SessionKey`
// is all three fields, and either of those is someone else's screen to lock.
assert!(!replaced_by(&conn(3, remote, key(8, "peer")), 2, &mine));
assert!(!replaced_by(&conn(3, remote, key(7, "other")), 2, &mine));
}
} }

View File

@@ -7,44 +7,24 @@ use std::{
/* /*
FPS adjust: FPS adjust:
a. new user connected => set to INIT_FPS a. new user connected =>set to INIT_FPS
b. TestDelay reply => update the user's fps from the excess delay, the reply's delay b. TestDelay receive => update user's fps according to network delay
above the baseline this connection has shown so far: When network delay < DELAY_THRESHOLD_150MS, set minimum fps according to image quality, and increase fps;
startup: two consecutive replies with excess < 50 ms permit doubling toward When network delay >= DELAY_THRESHOLD_150MS, set minimum fps according to image quality, and decrease fps;
the viewer's cap; a higher excess or a brake ends this acceleration; c. second timeout / TestDelay receive => update real fps to the minimum fps from all users
excess < DELAY_THRESHOLD_150MS: a good reply; grows the fps, and after a
reduction returns to the level held before it after two good replies;
excess >= DELAY_THRESHOLD_150MS: a bad reply; nothing happens until three in a
row confirm congestion, including after each reduction. FPS drops by a
fifth at most; a second of excess cannot wait and halves it immediately.
A recent fast restore also permits halving at 600 ms of excess.
While the bitrate can still be reduced (ABR) it is reduced first and the fps keeps
a floor: bitrate-targeted encoders do not send fewer bytes at fewer frames.
c. probe outstanding for more than two seconds => halve the fps for every further
second, down to MIN_AUTO_FPS and never above the target it found; the late
reply does not reduce again. Automatic reductions respect this floor unless
the viewer requested a lower FPS cap.
d. second timeout / TestDelay reply => real fps is the minimum over all users;
every user starts at INIT_FPS, adapts from its own target and is capped by its
own limit, never by that minimum or by another user's limit
ratio adjust: ratio adjust:
a. user set image quality => update to the maximum ratio of the latest quality a. user set image quality => update to the maximum ratio of the latest quality
b. 3 seconds timeout => update ratio according to network delay b. 3 seconds timeout => update ratio according to network delay
When network delay < DELAY_THRESHOLD_150MS, increase ratio, max 150kbps; When network delay < DELAY_THRESHOLD_150MS, increase ratio, max 150kbps;
When a user calls for a reduction (two bad replies in a row, or a probe still When network delay >= DELAY_THRESHOLD_150MS, decrease ratio;
out at the second tick past two seconds), decrease ratio by the step that user's
own delay and confirmation call for, the most conservative step over all users; adjust between FPS and ratio:
one slow reply or one short stall does not, and one user's spike is never paired When network delay < DELAY_THRESHOLD_150MS, fps is always higher than the minimum fps, and ratio is increasing;
with another user's confirmation. When network delay >= DELAY_THRESHOLD_150MS, fps is always lower than the minimum fps, and ratio is decreasing;
c. confirmed congestion => decrease ratio at once, when the 3 seconds cooldown allows
delay: delay:
TestDelay shares the video stream, so it measures the queue in front of it rather use delay minus RTT as the actual network delay
than the path RTT. The baseline starts at the first reply and follows lower
delays immediately. Old minima expire after 20 fresh replies; a higher window
minimum is learned gradually only when the recent floor is no longer rising.
Outstanding-probe checks and their late replies do not age this window.
*/ */
// Constants // Constants
@@ -52,7 +32,6 @@ pub const FPS: u32 = 30;
pub const MIN_FPS: u32 = 1; pub const MIN_FPS: u32 = 1;
pub const MAX_FPS: u32 = 120; pub const MAX_FPS: u32 = 120;
pub const INIT_FPS: u32 = 15; pub const INIT_FPS: u32 = 15;
const MIN_AUTO_FPS: u32 = 5;
// Bitrate ratio constants for different quality levels // Bitrate ratio constants for different quality levels
const BR_MAX: f32 = 40.0; // 2000 * 2 / 100 const BR_MAX: f32 = 40.0; // 2000 * 2 / 100
@@ -64,196 +43,45 @@ const HISTORY_DELAY_LEN: usize = 2;
const ADJUST_RATIO_INTERVAL: usize = 3; // Adjust quality ratio every 3 seconds const ADJUST_RATIO_INTERVAL: usize = 3; // Adjust quality ratio every 3 seconds
const DYNAMIC_SCREEN_THRESHOLD: usize = 2; // Allow increase quality ratio if encode more than 2 times in one second const DYNAMIC_SCREEN_THRESHOLD: usize = 2; // Allow increase quality ratio if encode more than 2 times in one second
const DELAY_THRESHOLD_150MS: u32 = 150; // 150ms is the threshold for good network condition const DELAY_THRESHOLD_150MS: u32 = 150; // 150ms is the threshold for good network condition
const RESTORE_GUARD_SAMPLES: u8 = 5; // A restored level that congests this soon is lowered
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
struct UserDelay { struct UserDelay {
stall_ticks: u8, // timer ticks the outstanding probe has been out beyond two seconds response_delayed: bool,
delay_history: VecDeque<u32>, delay_history: VecDeque<u32>,
fps: Option<u32>, fps: Option<u32>,
rtt_calculator: RttCalculator, rtt_calculator: RttCalculator,
quick_increase_fps_count: usize, quick_increase_fps_count: usize,
increase_fps_count: usize, increase_fps_count: usize,
consecutive_bad_samples: usize,
fps_bad_samples: u8, // fresh bad replies since the last FPS reduction
good_samples: usize, // since the last reduction, capped at 3
replies_after_bitrate_reduction: Option<u8>,
fps_before_congestion: Option<u32>, // level to return to once replies are good again
samples_since_restore: Option<u8>, // set by a restore, cleared once it proved stable
stall_reference_fps: Option<u32>, // fps when the outstanding probe passed two seconds
startup_good_samples: u8, // u8::MAX permanently ends startup acceleration
} }
impl UserDelay { impl UserDelay {
fn add_delay(&mut self, delay: u32) { fn add_delay(&mut self, delay: u32) {
if self.delay_history.len() >= HISTORY_DELAY_LEN { self.rtt_calculator.update(delay);
if self.delay_history.len() > HISTORY_DELAY_LEN {
self.delay_history.pop_front(); self.delay_history.pop_front();
} }
self.delay_history.push_back(delay); self.delay_history.push_back(delay);
} }
fn limit_fps_change( // Average delay minus RTT
&mut self,
current_fps: u32,
fps: u32,
delay: u32,
bitrate_first: bool,
braked: bool,
) -> u32 {
// A spike stays in the average for several samples; confirm congestion with fresh samples.
let delay = delay.saturating_sub(self.rtt_calculator.get_rtt().unwrap_or_default());
if let Some(samples) = self.samples_since_restore.as_mut() {
*samples = samples.saturating_add(1);
}
if delay < DELAY_THRESHOLD_150MS {
self.consecutive_bad_samples = 0;
self.fps_bad_samples = 0;
self.replies_after_bitrate_reduction = None;
self.good_samples = (self.good_samples + 1).min(3);
return self.recover(current_fps, fps);
}
self.consecutive_bad_samples = (self.consecutive_bad_samples + 1).min(3);
self.fps_bad_samples = (self.fps_bad_samples + 1).min(3);
if let Some(replies) = self.replies_after_bitrate_reduction.as_mut() {
*replies = (*replies + 1).min(2);
}
let failed_restore = delay >= 600
&& self
.samples_since_restore
.is_some_and(|samples| samples <= RESTORE_GUARD_SAMPLES);
// A level that congests right after being restored is not the level to return to.
if self
.samples_since_restore
.is_some_and(|samples| samples <= RESTORE_GUARD_SAMPLES)
&& (failed_restore || self.consecutive_bad_samples >= 3)
{
self.fps_before_congestion = Some(current_fps - current_fps / 4);
self.samples_since_restore = None;
}
// The timeout brake already reduced for the probe this reply answers.
if fps >= current_fps || braked {
return current_fps;
}
// An extra second of delay cannot wait for another confirmation.
if !failed_restore
&& delay < 1000
&& (self.fps_bad_samples < 3
|| (bitrate_first && self.replies_after_bitrate_reduction.unwrap_or_default() < 2))
{
return current_fps;
}
// A fast restore probes capacity. Roll it back promptly if the queue grows
// again, rather than waiting through another ordinary confirmation window.
let divisor = if delay >= 1000 || failed_restore {
2
} else {
5
};
self.on_reduction(current_fps);
fps.max(current_fps.saturating_sub((current_fps / divisor).max(1)))
}
// Fresh low-delay replies permit recovery even while the average contains a spike:
// a little at first, then back to the level held before congestion.
fn recover(&mut self, current_fps: u32, fps: u32) -> u32 {
let gradual = current_fps + (current_fps / 10).max(1);
let level = self
.fps_before_congestion
.filter(|level| *level > current_fps);
match (self.good_samples, level) {
(2 | 3, Some(level)) => {
self.fps_before_congestion = None;
self.samples_since_restore = Some(0);
fps.max(level)
}
(3, None) => {
self.fps_before_congestion = None;
fps.max(current_fps + (current_fps / 5).max(2))
}
_ => gradual,
}
}
fn accelerate_startup(
&mut self,
current_fps: u32,
fps: u32,
cap: u32,
delay: u32,
braked: bool,
) -> u32 {
if self.startup_good_samples == u8::MAX {
return fps;
}
let excess = delay.saturating_sub(self.rtt_calculator.get_rtt().unwrap_or_default());
// A low-load sample does not establish capacity: require two clean replies
// per step and abandon startup probing on the first sign of queue growth.
if braked || excess >= 50 || current_fps >= cap {
self.startup_good_samples = u8::MAX;
return fps;
}
self.startup_good_samples += 1;
if self.startup_good_samples < 2 {
return fps;
}
self.startup_good_samples = 0;
let accelerated = fps.max(current_fps.saturating_mul(2)).min(cap);
if accelerated >= cap {
self.startup_good_samples = u8::MAX;
}
accelerated
}
// The first reduction of an episode remembers the level to return to.
fn on_reduction(&mut self, current_fps: u32) {
self.startup_good_samples = u8::MAX;
self.good_samples = 0;
self.fps_bad_samples = 0;
if self.fps_before_congestion.is_none() {
self.fps_before_congestion = Some(current_fps);
}
}
// Bitrate is cut on confirmation only: two bad replies in a row, or a probe still
// outstanding at the second tick past two seconds. One slow reply or one short
// stall is jitter, and a static screen would never earn the cut back.
fn needs_bitrate_reduction(&self) -> bool {
self.consecutive_bad_samples >= 2 || self.stall_ticks >= 2
}
// The bitrate step this viewer's own evidence calls for, None when it calls for
// none. Severity and confirmation come from the same viewer; the controller
// never pairs one viewer's spike with another viewer's confirmation.
fn ratio_reduction(&self) -> Option<f32> {
if !self.needs_bitrate_reduction() {
return None;
}
let excess = self.avg_delay();
let confirmed = self.consecutive_bad_samples >= 3;
Some(if excess < 200 {
0.95
} else if excess < 300 {
0.9
} else if excess < 500 {
if confirmed {
0.7
} else {
0.85
}
} else if confirmed {
0.5
} else {
0.8
})
}
// Average delay above the baseline: what the queue adds on top of the path itself.
fn avg_delay(&self) -> u32 { fn avg_delay(&self) -> u32 {
if self.delay_history.is_empty() { let len = self.delay_history.len();
return DELAY_THRESHOLD_150MS; if len > 0 {
let avg_delay = self.delay_history.iter().sum::<u32>() / len as u32;
// If RTT is available, subtract it from average delay to get actual network latency
if let Some(rtt) = self.rtt_calculator.get_rtt() {
if avg_delay > rtt {
avg_delay - rtt
} else {
avg_delay
}
} else {
avg_delay
}
} else {
DELAY_THRESHOLD_150MS
} }
let avg_delay = self.delay_history.iter().sum::<u32>() / self.delay_history.len() as u32;
avg_delay.saturating_sub(self.rtt_calculator.get_rtt().unwrap_or_default())
} }
} }
@@ -265,20 +93,6 @@ struct UserData {
quality: Option<(i64, Quality)>, // (time, quality) quality: Option<(i64, Quality)>, // (time, quality)
delay: UserDelay, delay: UserDelay,
record: bool, record: bool,
joined_at: Option<Instant>, // set by on_connection_open; the start-up guard's clock
}
impl UserData {
// The frame rate this viewer asked for, from its custom or auto-adjust limit.
fn fps_cap(&self) -> u32 {
let mut fps = self.custom_fps.unwrap_or(FPS);
if let Some(auto_adjust_fps) = self.auto_adjust_fps {
if fps == 0 || auto_adjust_fps < fps {
fps = auto_adjust_fps;
}
}
fps.clamp(MIN_FPS, MAX_FPS)
}
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
@@ -296,9 +110,7 @@ pub struct VideoQoS {
bitrate_store: u32, bitrate_store: u32,
adjust_ratio_instant: Instant, adjust_ratio_instant: Instant,
abr_config: bool, abr_config: bool,
first_reply_adjusts_ratio: bool, // false on Linux, where it can create vaapi twice new_user_instant: Instant,
#[cfg(test)]
test_now: Option<Instant>,
} }
impl Default for VideoQoS { impl Default for VideoQoS {
@@ -311,33 +123,11 @@ impl Default for VideoQoS {
bitrate_store: 0, bitrate_store: 0,
adjust_ratio_instant: Instant::now(), adjust_ratio_instant: Instant::now(),
abr_config: true, abr_config: true,
first_reply_adjusts_ratio: !cfg!(target_os = "linux"), new_user_instant: Instant::now(),
#[cfg(test)]
test_now: None,
} }
} }
} }
// Clock; tests drive a virtual clock so timing is deterministic.
impl VideoQoS {
fn now(&self) -> Instant {
#[cfg(test)]
if let Some(now) = self.test_now {
return now;
}
Instant::now()
}
fn since(&self, instant: Instant) -> Duration {
self.now().saturating_duration_since(instant)
}
#[cfg(test)]
fn advance_ms(&mut self, ms: u64) {
self.test_now = Some(self.now() + Duration::from_millis(ms));
}
}
// Basic functionality // Basic functionality
impl VideoQoS { impl VideoQoS {
// Calculate seconds per frame based on current FPS // Calculate seconds per frame based on current FPS
@@ -394,12 +184,9 @@ impl VideoQoS {
impl VideoQoS { impl VideoQoS {
// Initialize new user session // Initialize new user session
pub fn on_connection_open(&mut self, id: i32) { pub fn on_connection_open(&mut self, id: i32) {
let user = UserData { self.users.insert(id, UserData::default());
joined_at: Some(self.now()),
..Default::default()
};
self.users.insert(id, user);
self.abr_config = Config::get_option("enable-abr") != "N"; self.abr_config = Config::get_option("enable-abr") != "N";
self.new_user_instant = Instant::now();
} }
// Clean up user session // Clean up user session
@@ -407,11 +194,7 @@ impl VideoQoS {
self.users.remove(&id); self.users.remove(&id);
if self.users.is_empty() { if self.users.is_empty() {
*self = Default::default(); *self = Default::default();
return;
} }
// The stream follows the remaining viewers at once; a departed viewer's
// start-up guard left with its entry.
self.adjust_fps();
} }
pub fn user_custom_fps(&mut self, id: i32, fps: u32) { pub fn user_custom_fps(&mut self, id: i32, fps: u32) {
@@ -461,10 +244,8 @@ impl VideoQoS {
} }
pub fn user_network_delay(&mut self, id: i32, delay: u32) { pub fn user_network_delay(&mut self, id: i32, delay: u32) {
let highest_fps = self.highest_fps();
let target_ratio = self.latest_quality().ratio(); let target_ratio = self.latest_quality().ratio();
// Fewer frames only save bytes with encoders that size frames for a fixed rate;
// bitrate-targeted encoders keep the bitrate, so the bitrate has to come down first.
let bitrate_first = self.can_reduce_bitrate();
// For bad network, small fps means quick reaction and high quality // For bad network, small fps means quick reaction and high quality
let (min_fps, normal_fps) = if target_ratio >= BR_BEST { let (min_fps, normal_fps) = if target_ratio >= BR_BEST {
@@ -479,25 +260,13 @@ impl VideoQoS {
let dividend_ms = DELAY_THRESHOLD_150MS * min_fps; let dividend_ms = DELAY_THRESHOLD_150MS * min_fps;
let mut adjust_ratio = false; let mut adjust_ratio = false;
let mut reduce_bitrate = false;
if let Some(user) = self.users.get_mut(&id) { if let Some(user) = self.users.get_mut(&id) {
let delay = delay.max(10); let delay = delay.max(10);
// The reply closes the outstanding probe, braked or not.
user.delay.stall_ticks = 0;
let braked = user.delay.stall_reference_fps.take().is_some();
let old_avg_delay = user.delay.avg_delay(); let old_avg_delay = user.delay.avg_delay();
if !braked {
user.delay.rtt_calculator.update(delay);
}
user.delay.add_delay(delay); user.delay.add_delay(delay);
let mut avg_delay = user.delay.avg_delay(); let mut avg_delay = user.delay.avg_delay();
avg_delay = avg_delay.max(10); avg_delay = avg_delay.max(10);
// Each viewer adapts from its own target, starts at INIT_FPS and is capped let mut fps = self.fps;
// by its own limit. The stream follows the slowest viewer in adjust_fps;
// neither that minimum nor another viewer's limit feeds back into it.
let user_cap = user.fps_cap();
let current_fps = user.delay.fps.unwrap_or(INIT_FPS.min(user_cap));
let mut fps = current_fps;
// Adaptive FPS adjustment based on network delay: // Adaptive FPS adjustment based on network delay:
if avg_delay < 50 { if avg_delay < 50 {
@@ -552,84 +321,26 @@ impl VideoQoS {
user.delay.quick_increase_fps_count = 0; user.delay.quick_increase_fps_count = 0;
} }
if bitrate_first { fps = fps.clamp(MIN_FPS, highest_fps);
// While the bitrate can still come down, the frame rate keeps its floor.
fps = fps.max(min_fps);
}
fps = fps.max(MIN_AUTO_FPS.min(user_cap));
fps = user
.delay
.limit_fps_change(current_fps, fps, delay, bitrate_first, braked);
fps = user
.delay
.accelerate_startup(current_fps, fps, user_cap, delay, braked);
reduce_bitrate = bitrate_first
&& user.delay.needs_bitrate_reduction()
&& user.delay.replies_after_bitrate_reduction.is_none();
fps = fps.clamp(MIN_FPS, user_cap);
// first network delay message // first network delay message
adjust_ratio = user.delay.fps.is_none(); adjust_ratio = user.delay.fps.is_none();
user.delay.fps = Some(fps); user.delay.fps = Some(fps);
let base = user.delay.rtt_calculator.get_rtt().unwrap_or_default();
log::trace!(
"qos_trace t={} id={id} delay={delay} base={base} excess={} avg={avg_delay} bad={} good={} braked={braked} fps={fps} ratio={:.3} reduce_bitrate={reduce_bitrate}",
hbb_common::get_time(),
delay.saturating_sub(base),
user.delay.consecutive_bad_samples,
user.delay.good_samples,
self.ratio,
);
} }
self.adjust_fps(); self.adjust_fps();
// A viewer's first reply is one more trigger of the periodic adjustment and if adjust_ratio && !cfg!(target_os = "linux") {
// keeps its cooldown: a viewer joining right after a cut must not spend the //Reduce the possibility of vaapi being created twice
// other viewers' evidence a second time.
if adjust_ratio
&& self.first_reply_adjusts_ratio
&& self.since(self.adjust_ratio_instant).as_secs() >= ADJUST_RATIO_INTERVAL as u64
{
self.adjust_ratio(false);
}
if reduce_bitrate
&& self.since(self.adjust_ratio_instant).as_secs() >= ADJUST_RATIO_INTERVAL as u64
{
self.adjust_ratio(false); self.adjust_ratio(false);
} }
} }
pub fn user_delay_response_elapsed(&mut self, id: i32, elapsed: u128) { pub fn user_delay_response_elapsed(&mut self, id: i32, elapsed: u128) {
let Some(user) = self.users.get_mut(&id) else { if let Some(user) = self.users.get_mut(&id) {
return; user.delay.response_delayed = elapsed > 2000;
}; if user.delay.response_delayed {
if elapsed <= 2000 { user.delay.add_delay(elapsed as u32);
return; self.adjust_fps();
}
user.delay.stall_ticks = user.delay.stall_ticks.saturating_add(1);
user.delay.add_delay(elapsed as u32);
// Halve for every second the probe stays out beyond the first: two seconds
// halve, three quarter, and so on down to the floor.
let reference = match user.delay.stall_reference_fps {
Some(reference) => reference,
None => {
let reference = user.delay.fps.unwrap_or(INIT_FPS.min(user.fps_cap()));
user.delay.stall_reference_fps = Some(reference);
user.delay.on_reduction(reference);
reference
} }
}; }
let divisor = 1u32 << ((elapsed / 1000) as u32).saturating_sub(1).min(5);
let user_cap = user.fps_cap();
// The floor is a floor, not a lift: a target already below it stays.
let current = user.delay.fps.unwrap_or(reference);
let fps = (reference / divisor)
.clamp(MIN_AUTO_FPS.min(user_cap), user_cap)
.min(current);
user.delay.fps = Some(fps);
log::debug!(
"qos_trace t={} id={id} timeout={elapsed} fps={fps}",
hbb_common::get_time()
);
self.adjust_fps();
} }
} }
@@ -651,11 +362,14 @@ impl VideoQoS {
self.adjust_fps(); self.adjust_fps();
let abr_enabled = self.in_vbr_state(); let abr_enabled = self.in_vbr_state();
if abr_enabled { if abr_enabled {
if self.since(self.adjust_ratio_instant).as_secs() >= ADJUST_RATIO_INTERVAL as u64 { if self.adjust_ratio_instant.elapsed().as_secs() >= ADJUST_RATIO_INTERVAL as u64 {
let dynamic_screen = self let dynamic_screen = self
.displays .displays
.iter() .iter()
.any(|d| d.1.send_counter >= ADJUST_RATIO_INTERVAL * DYNAMIC_SCREEN_THRESHOLD); .any(|d| d.1.send_counter >= ADJUST_RATIO_INTERVAL * DYNAMIC_SCREEN_THRESHOLD);
self.displays.iter_mut().for_each(|d| {
d.1.send_counter = 0;
});
self.adjust_ratio(dynamic_screen); self.adjust_ratio(dynamic_screen);
} }
} else { } else {
@@ -665,12 +379,25 @@ impl VideoQoS {
#[inline] #[inline]
fn highest_fps(&self) -> u32 { fn highest_fps(&self) -> u32 {
self.users let user_fps = |u: &UserData| {
.values() let mut fps = u.custom_fps.unwrap_or(FPS);
.map(|u| u.fps_cap()) if let Some(auto_adjust_fps) = u.auto_adjust_fps {
if fps == 0 || auto_adjust_fps < fps {
fps = auto_adjust_fps;
}
}
fps
};
let fps = self
.users
.iter()
.map(|(_, u)| user_fps(u))
.filter(|u| *u >= MIN_FPS)
.min() .min()
.unwrap_or(FPS) .unwrap_or(FPS);
.clamp(MIN_FPS, MAX_FPS)
fps.clamp(MIN_FPS, MAX_FPS)
} }
// Get latest quality settings from all users // Get latest quality settings from all users
@@ -685,16 +412,40 @@ impl VideoQoS {
.1 .1
} }
// Lowest ratio the latest quality allows: keeps about 1Mbps at high resolutions. // Adjust quality ratio based on network delay and screen changes
fn min_ratio(&self) -> f32 { fn adjust_ratio(&mut self, dynamic_screen: bool) {
if !self.in_vbr_state() {
return;
}
// Get maximum delay from all users
let max_delay = self.users.iter().map(|u| u.1.delay.avg_delay()).max();
let Some(max_delay) = max_delay else {
return;
};
let target_quality = self.latest_quality();
let target_ratio = self.latest_quality().ratio();
let current_ratio = self.ratio;
let current_bitrate = self.bitrate(); let current_bitrate = self.bitrate();
// Calculate minimum ratio for high resolution (1Mbps baseline)
let ratio_1mbps = if current_bitrate > 0 { let ratio_1mbps = if current_bitrate > 0 {
Some((self.ratio * 1000.0 / current_bitrate as f32).max(BR_MIN_HIGH_RESOLUTION)) Some((current_ratio * 1000.0 / current_bitrate as f32).max(BR_MIN_HIGH_RESOLUTION))
} else { } else {
None None
}; };
match self.latest_quality() {
// Calculate ratio for adding 150kbps bandwidth
let ratio_add_150kbps = if current_bitrate > 0 {
Some((current_bitrate + 150) as f32 * current_ratio / current_bitrate as f32)
} else {
None
};
// Set minimum ratio based on quality mode
let min = match target_quality {
Quality::Best => { Quality::Best => {
// For Best quality, ensure minimum 1Mbps for high resolution
let mut min = BR_BEST / 2.5; let mut min = BR_BEST / 2.5;
if let Some(ratio_1mbps) = ratio_1mbps { if let Some(ratio_1mbps) = ratio_1mbps {
if min > ratio_1mbps { if min > ratio_1mbps {
@@ -712,67 +463,15 @@ impl VideoQoS {
} }
min.max(BR_MIN_HIGH_RESOLUTION) min.max(BR_MIN_HIGH_RESOLUTION)
} }
Quality::Low | Quality::Custom(_) => BR_MIN_HIGH_RESOLUTION, Quality::Low => BR_MIN_HIGH_RESOLUTION,
} Quality::Custom(_) => BR_MIN_HIGH_RESOLUTION,
}
// Whether congestion can still be answered with a lower bitrate. Within two
// percent of the floor another step is not worth waiting a cooldown for.
fn can_reduce_bitrate(&self) -> bool {
self.in_vbr_state() && !self.displays.is_empty() && self.ratio > self.min_ratio() * 1.02
}
// Every ratio adjustment starts a new window for the dynamic screen counters.
fn reset_send_counters(&mut self) {
self.displays.values_mut().for_each(|d| d.send_counter = 0);
}
// Adjust quality ratio based on network delay and screen changes
fn adjust_ratio(&mut self, dynamic_screen: bool) {
if !self.in_vbr_state() {
return;
}
// Get maximum delay from all users
let max_delay = self.users.iter().map(|u| u.1.delay.avg_delay()).max();
let Some(max_delay) = max_delay else {
return;
}; };
// Each viewer judges its own delay; the stream takes the most conservative
// step any viewer asks for.
let reduction = self
.users
.values()
.filter_map(|u| u.delay.ratio_reduction())
.reduce(f32::min);
if reduction.is_none() && max_delay >= DELAY_THRESHOLD_150MS {
// Elevated but unconfirmed: no change, and no cooldown either, so a
// confirmation on the next reply is acted on at once.
self.reset_send_counters();
return;
}
let target_ratio = self.latest_quality().ratio();
let current_ratio = self.ratio;
let current_bitrate = self.bitrate();
// Calculate ratio for adding 150kbps bandwidth
let ratio_add_150kbps = if current_bitrate > 0 {
Some((current_bitrate + 150) as f32 * current_ratio / current_bitrate as f32)
} else {
None
};
let min = self.min_ratio();
let max = target_ratio * MAX_BR_MULTIPLE; let max = target_ratio * MAX_BR_MULTIPLE;
let mut v = current_ratio; let mut v = current_ratio;
// Three bad replies in a row confirm congestion; with a bitrate-targeted // Adjust ratio based on network delay thresholds
// encoder the bitrate is then the only thing that drains the queue, so it if max_delay < 50 {
// comes down hard. Increases need every viewer below the threshold.
if let Some(factor) = reduction {
v = current_ratio * factor;
} else if max_delay < 50 {
if dynamic_screen { if dynamic_screen {
v = current_ratio * 1.15; v = current_ratio * 1.15;
} }
@@ -780,8 +479,18 @@ impl VideoQoS {
if dynamic_screen { if dynamic_screen {
v = current_ratio * 1.1; v = current_ratio * 1.1;
} }
} else if dynamic_screen { } else if max_delay < DELAY_THRESHOLD_150MS {
v = current_ratio * 1.05; if dynamic_screen {
v = current_ratio * 1.05;
}
} else if max_delay < 200 {
v = current_ratio * 0.95;
} else if max_delay < 300 {
v = current_ratio * 0.9;
} else if max_delay < 500 {
v = current_ratio * 0.85;
} else {
v = current_ratio * 0.8;
} }
// Limit quality increase rate for better stability // Limit quality increase rate for better stability
@@ -794,24 +503,8 @@ impl VideoQoS {
} }
} }
if reduction.is_some() {
for user in self.users.values_mut() {
if user.delay.needs_bitrate_reduction()
&& user.delay.replies_after_bitrate_reduction.is_none()
{
// One outstanding probe may have started before the bitrate change.
user.delay.replies_after_bitrate_reduction =
Some(if v.clamp(min, max) < current_ratio {
0
} else {
2
});
}
}
}
self.ratio = v.clamp(min, max); self.ratio = v.clamp(min, max);
self.reset_send_counters(); self.adjust_ratio_instant = Instant::now();
self.adjust_ratio_instant = self.now();
} }
// Adjust fps based on network delay and user response time // Adjust fps based on network delay and user response time
@@ -825,13 +518,17 @@ impl VideoQoS {
.min() .min()
.unwrap_or(INIT_FPS); .unwrap_or(INIT_FPS);
// Every viewer inside its first second keeps the stream at INIT_FPS to if self.users.iter().any(|u| u.1.delay.response_delayed) {
// ensure stability; each viewer carries its own start-up clock. if fps > MIN_FPS + 1 {
if self.users.values().any(|u| { fps = MIN_FPS + 1;
u.joined_at }
.is_some_and(|joined| self.since(joined).as_secs() < 1) }
}) {
fps = fps.min(INIT_FPS); // For new connections (within 1 second), cap fps to INIT_FPS to ensure stability
if self.new_user_instant.elapsed().as_secs() < 1 {
if fps > INIT_FPS {
fps = INIT_FPS;
}
} }
// Ensure fps stays within valid range // Ensure fps stays within valid range
@@ -841,183 +538,58 @@ impl VideoQoS {
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
struct RttCalculator { struct RttCalculator {
baseline: Option<u32>, min_rtt: Option<u32>, // Historical minimum RTT ever observed
samples: VecDeque<u32>, window_min_rtt: Option<u32>, // Minimum RTT within last 60 samples
smoothed_rtt: Option<u32>, // Smoothed RTT estimation
samples: VecDeque<u32>, // Last 60 RTT samples
} }
impl RttCalculator { impl RttCalculator {
const WINDOW_SAMPLES: usize = 20; const WINDOW_SAMPLES: usize = 60; // Keep last 60 samples
const MAX_INCREASE_MS: u32 = 50; const MIN_SAMPLES: usize = 10; // Require at least 10 samples
const ALPHA: f32 = 0.5; // Smoothing factor for weighted average
/// Update RTT estimates with a new sample
pub fn update(&mut self, delay: u32) { pub fn update(&mut self, delay: u32) {
// 1. Update historical minimum RTT
match self.min_rtt {
Some(min_rtt) if delay < min_rtt => self.min_rtt = Some(delay),
None => self.min_rtt = Some(delay),
_ => {}
}
// 2. Update sample window
if self.samples.len() >= Self::WINDOW_SAMPLES { if self.samples.len() >= Self::WINDOW_SAMPLES {
self.samples.pop_front(); self.samples.pop_front();
} }
self.samples.push_back(delay); self.samples.push_back(delay);
let baseline = self.baseline.unwrap_or(delay).min(delay);
self.baseline = Some(baseline);
if self.samples.len() < Self::WINDOW_SAMPLES { // 3. Calculate minimum RTT within the window
return; self.window_min_rtt = self.samples.iter().min().copied();
}
let half = Self::WINDOW_SAMPLES / 2; // 4. Calculate smoothed RTT
let older_min = self.samples.iter().take(half).min().copied(); // Use weighted average if we have enough samples
let recent_min = self.samples.iter().skip(half).min().copied(); if self.samples.len() >= Self::WINDOW_SAMPLES {
let (Some(older_min), Some(recent_min)) = (older_min, recent_min) else { if let (Some(min), Some(window_min)) = (self.min_rtt, self.window_min_rtt) {
return; // Weighted average of historical minimum and window minimum
}; let new_srtt =
// A rising floor can be a growing queue. Allow 10 ms of probe granularity, ((1.0 - Self::ALPHA) * min as f32 + Self::ALPHA * window_min as f32) as u32;
// but wait for it to settle before forgetting the old baseline. self.smoothed_rtt = Some(new_srtt);
if recent_min > older_min.saturating_add(10) { }
return;
}
let rise = older_min.min(recent_min).saturating_sub(baseline);
if rise > 0 {
self.baseline = Some(baseline + (rise / 2).clamp(1, Self::MAX_INCREASE_MS));
} }
} }
/// Get current RTT estimate
/// Returns None if no valid estimation is available
pub fn get_rtt(&self) -> Option<u32> { pub fn get_rtt(&self) -> Option<u32> {
self.baseline if let Some(rtt) = self.smoothed_rtt {
return Some(rtt);
}
if self.samples.len() >= Self::MIN_SAMPLES {
if let Some(rtt) = self.min_rtt {
return Some(rtt);
}
}
None
} }
} }
#[cfg(test)]
mod tests {
use super::*;
fn stable_qos() -> VideoQoS {
let mut qos = VideoQoS::default();
qos.advance_ms(2000);
qos.users.insert(1, UserData::default());
for _ in 0..12 {
qos.user_network_delay(1, 10);
}
assert_eq!(qos.fps(), FPS);
qos
}
#[test]
fn isolated_delay_spike_does_not_lower_fps() {
let mut qos = stable_qos();
for delay in [800, 10, 10, 10] {
qos.user_network_delay(1, delay);
assert_eq!(qos.fps(), FPS);
}
}
#[test]
fn occasional_spikes_do_not_accumulate_congestion() {
let mut qos = stable_qos();
for delay in [800, 10, 10].repeat(20) {
qos.user_network_delay(1, delay);
assert_eq!(qos.fps(), FPS);
}
}
#[test]
fn sustained_delay_reduces_fps_gradually() {
let mut qos = stable_qos();
for expected_fps in [30, 30, 24, 24, 24, 20] {
qos.user_network_delay(1, 800);
assert_eq!(qos.fps(), expected_fps);
}
}
#[test]
fn delay_history_keeps_two_samples() {
let mut delay = UserDelay::default();
for sample in [1, 2, 3] {
delay.add_delay(sample);
}
assert_eq!(delay.delay_history.len(), HISTORY_DELAY_LEN);
}
#[test]
fn response_timeout_halves_fps_for_each_second_outstanding() {
let mut qos = stable_qos();
for (elapsed, expected) in [(2001, 15), (3001, 7), (4001, 5), (5001, 5), (6001, 5)] {
qos.user_delay_response_elapsed(1, elapsed);
assert_eq!(qos.fps(), expected, "{elapsed} ms outstanding");
}
}
#[test]
fn severe_delay_does_not_wait_for_another_reply() {
let mut qos = stable_qos();
qos.user_network_delay(1, 1200);
assert_eq!(qos.fps(), 15);
}
#[test]
fn response_timeout_recovers_in_two_good_replies() {
let mut qos = stable_qos();
qos.user_delay_response_elapsed(1, 3000);
assert_eq!(qos.fps(), 7);
qos.user_network_delay(1, 3200);
assert_eq!(
qos.fps(),
7,
"the late reply belongs to the stall that was braked"
);
qos.user_delay_response_elapsed(1, 0);
qos.user_network_delay(1, 10);
assert_eq!(
qos.fps(),
8,
"one good reply must not restore the full frame rate"
);
qos.user_network_delay(1, 10);
assert_eq!(
qos.fps(),
FPS,
"the second good reply restores the frame rate"
);
qos.user_network_delay(1, 10);
assert_eq!(qos.fps(), FPS, "the third keeps the restored frame rate");
}
#[test]
fn restore_aims_lower_after_a_restore_that_congested() {
let mut qos = stable_qos();
for _ in 0..2 {
qos.user_network_delay(1, 1200);
}
assert_eq!(qos.fps(), 8);
qos.user_network_delay(1, 10);
qos.user_network_delay(1, 10);
assert_eq!(qos.fps(), FPS);
// The restored level congests at once, so the next restore aims lower.
for _ in 0..3 {
qos.user_network_delay(1, 400);
}
assert!(qos.fps() < FPS);
qos.user_network_delay(1, 10);
qos.user_network_delay(1, 10);
assert!(
qos.fps() < FPS,
"no return to the level that failed: {}",
qos.fps()
);
qos.user_network_delay(1, 10);
assert_eq!(qos.fps(), FPS, "ordinary recovery can still reach the cap");
}
#[test]
fn custom_fps_limit_applies_during_delay_spike() {
let mut qos = stable_qos();
qos.user_custom_fps(1, 12);
qos.user_network_delay(1, 800);
assert_eq!(qos.fps(), 12);
}
mod adaptation;
mod baseline;
mod invariants;
mod jitter;
mod recovery;
mod robustness;
mod sim;
mod smoke;
mod startup;
}

View File

@@ -1,353 +0,0 @@
//! Regression tests for sustained capacity drops and path-delay/content changes.
//! Capacity drops use the closed-loop model; delay and activity fixtures are open-loop.
use super::*;
fn percentile(values: &[f64], p: f64) -> f64 {
assert!(!values.is_empty());
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
sorted[((sorted.len() - 1) as f64 * p).round() as usize]
}
// Count a fall and subsequent rise of at least `amplitude`, ignoring smaller
// reversals. A one-way reduction or recovery is not an oscillation.
fn round_trips(values: &[f64], amplitude: f64) -> usize {
let mut peak = values[0];
let mut trough = peak;
let mut falling = false;
let mut count = 0;
for &value in &values[1..] {
if falling {
trough = trough.min(value);
if value - trough >= amplitude {
count += 1;
peak = value;
falling = false;
}
} else {
peak = peak.max(value);
if peak - value >= amplitude {
trough = value;
falling = true;
}
}
}
count
}
#[test]
fn oscillation_metric_distinguishes_recovery_and_small_jitter() {
assert_eq!(round_trips(&[30.0, 29.0, 30.0, 28.0, 30.0], 7.5), 0);
assert_eq!(round_trips(&[30.0, 20.0, 10.0], 7.5), 0);
assert_eq!(round_trips(&[10.0, 20.0, 30.0], 7.5), 0);
assert_eq!(round_trips(&[30.0, 10.0, 30.0, 20.0, 30.0], 7.5), 2);
assert_eq!(round_trips(&[0.49, 0.17, 0.49], 0.67 * 0.25), 1);
}
struct Oscillation {
mean_fps: f64,
mean_ratio: f64,
fps_span: f64,
ratio_span: f64,
fps_cycles_per_min: f64,
ratio_cycles_per_min: f64,
queue_p95_ms: f64,
}
fn permanent_drop(seeds: std::ops::RangeInclusive<u64>) {
use super::sim::{self, Summary};
const STEADY_START_MS: u32 = 120_000;
const END_MS: u32 = 600_000;
let cases = [
("bandwidth_halved_30", 3000.0),
("bandwidth_halved_fixed_rate_30", 2000.0),
("bandwidth_halved_fixed_rate_no_abr_30", 3000.0),
];
println!("Permanent drop: 8 -> 2.5 Mbps at 60 s; duration 600 s; seeds {seeds:?}.");
println!("Oscillation/queue window: 120-600 s. Spans are p95-p5; round trips require 25% of the requested FPS/ratio in each direction. Delivered FPS/frame age cover 15-600 s.");
println!("| scenario | capacity wobble | steady mean FPS (median) | mean ratio (median) | FPS span (p90) | ratio span (p90) | FPS cycles/min (p90) | ratio cycles/min (p90) | queue p95 (p90) | delivered FPS (median) | frame age p95 (p90) |");
println!("|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|");
let mut cases_run = 0;
for mut sc in sim::scenarios() {
let Some((_, queue_bound_ms)) = cases.iter().find(|(name, _)| *name == sc.name) else {
continue;
};
cases_run += 1;
sc.seconds = END_MS / 1000;
sc.link.capacity_kbps = vec![(0, 8000.0), (60_000, 2500.0)];
let original_wobble = sc.link.wobble;
for wobble in [0.0, original_wobble] {
sc.link.wobble = wobble;
let mut reports = Vec::new();
let mut oscillations = Vec::new();
for seed in seeds.clone() {
sc.seed = seed;
let report = sim::run(&sc);
assert!(
report
.trace
.iter()
.filter(|(t, ..)| (30_000..60_000).contains(t))
.all(|(_, fps, _, _)| *fps == sc.limit),
"healthy pre-drop phase: {} seed {seed}",
sc.name
);
let steady: Vec<_> = report
.trace
.iter()
.filter(|(t, ..)| *t >= STEADY_START_MS)
.collect();
let fps: Vec<_> = steady.iter().map(|(_, fps, _, _)| *fps as f64).collect();
let ratios: Vec<_> = steady
.iter()
.map(|(_, _, _, ratio)| *ratio as f64)
.collect();
let queues: Vec<_> = steady
.iter()
.map(|(_, _, queue, _)| *queue as f64)
.collect();
let minutes = (END_MS - STEADY_START_MS) as f64 / 60_000.0;
oscillations.push(Oscillation {
mean_fps: fps.iter().sum::<f64>() / fps.len() as f64,
mean_ratio: ratios.iter().sum::<f64>() / ratios.len() as f64,
fps_span: percentile(&fps, 0.95) - percentile(&fps, 0.05),
ratio_span: percentile(&ratios, 0.95) - percentile(&ratios, 0.05),
fps_cycles_per_min: round_trips(&fps, sc.limit as f64 * 0.25) as f64 / minutes,
ratio_cycles_per_min: round_trips(&ratios, sc.quality.ratio() as f64 * 0.25)
as f64
/ minutes,
queue_p95_ms: percentile(&queues, 0.95),
});
reports.push(report);
}
let metric = |field: fn(&Oscillation) -> f64, p| {
percentile(&oscillations.iter().map(field).collect::<Vec<_>>(), p)
};
let summary = Summary::of(&reports);
let queue_p95 = metric(|o| o.queue_p95_ms, 0.9);
println!("| {} | {:.0}% | {:.1} | {:.3} | {:.1} | {:.3} | {:.2} | {:.2} | {:.0} ms | {:.1} | {} ms |",
sc.name, wobble * 100.0, metric(|o| o.mean_fps, 0.5),
metric(|o| o.mean_ratio, 0.5),
metric(|o| o.fps_span, 0.9), metric(|o| o.ratio_span, 0.9),
metric(|o| o.fps_cycles_per_min, 0.9), metric(|o| o.ratio_cycles_per_min, 0.9),
queue_p95, summary.delivered_median, summary.frame_age_p95_p90);
// Queue and frame-age limits reuse the transient-drop budgets;
// oscillation statistics are diagnostic.
assert!(
queue_p95 < *queue_bound_ms,
"{}: steady queue {queue_p95} ms",
sc.name
);
assert!(
(summary.frame_age_p95_p90 as f64) < *queue_bound_ms,
"{summary:?}"
);
assert!(
summary.delivered_median >= sc.limit as f64 * 0.4,
"{summary:?}"
);
}
}
assert_eq!(cases_run, cases.len());
}
#[test]
fn permanent_capacity_drop_600s() {
permanent_drop(super::sim::SEEDS);
}
#[test]
#[ignore = "extended permanent-drop coverage over 100 held-out seeds"]
fn permanent_capacity_drop_held_out_seeds() {
permanent_drop(21..=120);
}
#[test]
fn low_capacity_preserves_auto_floor_and_recovers() {
let mut sc = super::sim::scenarios()
.into_iter()
.find(|s| s.name == "bandwidth_halved_fixed_rate_no_abr_30")
.unwrap();
sc.link.capacity_kbps = vec![(0, 8000.0), (60_000, 700.0), (120_000, 8000.0)];
for seed in super::sim::SEEDS {
sc.seed = seed;
let report = super::sim::run(&sc);
assert!(
report.trace.iter().all(|(_, fps, ..)| *fps >= 5),
"seed {seed}: automatic reductions went below 5 FPS"
);
let congested: Vec<_> = report
.trace
.iter()
.filter(|(t, ..)| (80_000..120_000).contains(t))
.collect();
let min_fps = congested.iter().map(|(_, fps, ..)| *fps).min().unwrap();
let min_queue = congested
.iter()
.map(|(_, _, queue, _)| *queue)
.min()
.unwrap();
assert_eq!(
min_fps, 5,
"seed {seed}: severe congestion must reach the floor"
);
// At 5 FPS this model sends about 670 kbps, leaving little room to drain
// existing backlog at 700 kbps. Require drainage after capacity returns.
assert!(
report.recovery_ms.is_some_and(|ms| ms <= 20_000),
"seed {seed}: recovery took {:?}",
report.recovery_ms
);
println!("700 kbps fixed-rate, seed {seed}: min FPS={min_fps}, min queue={min_queue} ms, queue p95={} ms, frame age p95={} ms, recovery={:?}", report.queue_p95_ms, report.frame_age_p95_ms, report.recovery_ms);
}
}
const DISPLAY: &str = "adaptation";
fn session(abr: bool) -> VideoQoS {
let mut qos = super::smoke::session(FPS, Quality::Balanced);
qos.abr_config = abr;
qos.new_display(DISPLAY.to_owned());
qos.set_support_changing_quality(DISPLAY, true);
sync_bitrate(&mut qos);
qos
}
fn sync_bitrate(qos: &mut VideoQoS) {
let bitrate = (6000.0 * qos.ratio()) as u32;
qos.store_bitrate(bitrate);
}
fn second(qos: &mut VideoQoS, delay: u32, dynamic: bool) {
let encoded = if dynamic { qos.fps() as usize } else { 0 };
qos.advance_ms(1000);
sync_bitrate(qos);
qos.user_network_delay(1, delay);
sync_bitrate(qos);
qos.update_display_data(DISPLAY, encoded);
sync_bitrate(qos);
}
fn baseline_steps() -> Vec<String> {
let mut unmet = Vec::new();
println!("Baseline step: 90 s at 10 ms, 180 s at new RTT, 90 s at 10 ms; one fresh reply per second. Relearning requires FPS=30 and excess<150 ms throughout the final 60 s at the new RTT.");
println!("| ABR | new RTT | cold-start final FPS | learned baseline | final excess | final FPS | final ratio | relearned | returned-path FPS |");
println!("|---|---:|---:|---:|---:|---:|---:|---|---:|");
for abr in [false, true] {
for rtt in [310, 410] {
let mut cold = session(abr);
for _ in 0..90 {
second(&mut cold, rtt, true);
assert!(cold.fps() >= INIT_FPS, "stable cold-start RTT {rtt}");
}
assert_eq!(cold.fps(), FPS);
let mut qos = session(abr);
for _ in 0..90 {
second(&mut qos, 10, true);
}
assert_eq!(qos.fps(), FPS);
let mut relearned = true;
for s in 0..180 {
second(&mut qos, rtt, true);
if s >= 120 {
let base = qos.users[&1].delay.rtt_calculator.get_rtt().unwrap();
relearned &= qos.fps() == FPS && rtt.saturating_sub(base) < 150;
}
}
let base = qos.users[&1].delay.rtt_calculator.get_rtt().unwrap();
let high_fps = qos.fps();
let high_ratio = qos.ratio();
for _ in 0..90 {
second(&mut qos, 10, true);
}
assert_eq!(
qos.fps(),
FPS,
"return to the original path: ABR={abr} RTT={rtt}"
);
assert!(qos.ratio() >= BR_BALANCED * 0.95);
println!("| {abr} | {rtt} ms | {} | {base} ms | {} ms | {high_fps} | {high_ratio:.3} | {relearned} | {} |",
cold.fps(), rtt.saturating_sub(base), qos.fps());
if !relearned {
unmet.push(format!(
"ABR={abr}, RTT 10 -> {rtt} ms: base={base}, FPS={high_fps}"
));
}
}
}
unmet
}
#[test]
fn baseline_step_relearns_higher_rtt() {
let unmet = baseline_steps();
assert!(
unmet.is_empty(),
"higher baseline was not relearned: {unmet:?}"
);
}
#[test]
fn static_to_dynamic_ratio_recovery() {
println!("Static recovery: 90 s healthy video, 12 s confirmed 800 ms delay, 60 s healthy static screen, then 90 s video. Bitrate is modeled as ratio * 6000 kbps.");
println!("| restart profile | ratio after cut | ratio after static | time to 90% | time to 95% | final ratio | final FPS | modeled bitrate |");
println!("|---|---:|---:|---|---|---:|---:|---:|");
for (profile, restart_delay, growing_queue) in [
("healthy 10 ms", 10, false),
("stable path 800 ms", 800, false),
("growing queue 800 + 10 ms/s", 800, true),
] {
let mut qos = session(true);
for _ in 0..90 {
second(&mut qos, 10, true);
}
let target = qos.latest_quality().ratio();
assert_eq!(qos.ratio(), target);
for _ in 0..12 {
second(&mut qos, 800, true);
}
let after_cut = qos.ratio();
assert!(
after_cut < target * 0.5,
"fixture must confirm congestion and cut bitrate"
);
for _ in 0..60 {
second(&mut qos, 10, false);
}
let after_static = qos.ratio();
let mut t90 = (after_static >= target * 0.90).then_some(0);
let mut t95 = (after_static >= target * 0.95).then_some(0);
for s in 1..=90 {
let delay = restart_delay + if growing_queue { s * 10 } else { 0 };
second(&mut qos, delay, true);
let ratio = qos.ratio();
if ratio >= target * 0.90 {
t90.get_or_insert(s);
}
if ratio >= target * 0.95 {
t95.get_or_insert(s);
}
if growing_queue {
assert!(
ratio <= after_static * 1.02,
"activity must not restore quality into congestion"
);
}
}
let seconds = |time: Option<u32>| {
time.map(|s| format!("{s} s"))
.unwrap_or_else(|| "never".to_owned())
};
let ratio = qos.ratio();
println!("| {profile} | {after_cut:.3} | {after_static:.3} | {} | {} | {ratio:.3} | {} | {} kbps |",
seconds(t90), seconds(t95), qos.fps(), qos.bitrate());
if !growing_queue {
assert!(
t95.is_some(),
"{profile}: video did not regain 95% quality within 90 s"
);
assert_eq!(qos.fps(), FPS);
}
}
}

View File

@@ -1,110 +0,0 @@
use super::*;
fn learned_baseline(delay: u32) -> RttCalculator {
let mut rtt = RttCalculator::default();
for _ in 0..90 {
rtt.update(delay);
}
rtt
}
#[test]
fn old_minimum_expires_after_a_stable_path_change() {
for (old, new) in [(10, 310), (10, 500), (159, 500)] {
let mut rtt = learned_baseline(old);
for i in 0..40 {
let before = rtt.get_rtt().unwrap();
rtt.update(new + i % 5 * 10);
let after = rtt.get_rtt().unwrap();
assert!(after <= before + 50, "limit the cost of relearning");
if i < 10 {
assert_eq!(after, old, "a short burst must not replace the baseline");
}
}
assert_eq!(rtt.get_rtt(), Some(new), "{old} -> {new}");
rtt.update(old);
assert_eq!(rtt.get_rtt(), Some(old), "a lower delay is direct evidence");
}
}
#[test]
fn rising_delay_is_not_learned_as_a_new_baseline() {
for step in [2, 10, 50] {
let mut rtt = learned_baseline(10);
for i in 0..120 {
rtt.update(200 + i * step);
assert_eq!(rtt.get_rtt(), Some(10), "rising by {step} ms per reply");
}
}
}
#[test]
fn intermittent_spikes_do_not_raise_the_baseline() {
let mut rtt = learned_baseline(159);
for delay in [159, 900, 500, 159, 350].repeat(30) {
rtt.update(delay);
assert_eq!(rtt.get_rtt(), Some(159));
}
}
#[test]
fn pending_probes_and_late_replies_do_not_age_the_baseline() {
let mut qos = stable_qos();
for elapsed in (2001..122_001).step_by(1000) {
qos.user_delay_response_elapsed(1, elapsed);
assert_eq!(qos.users[&1].delay.rtt_calculator.get_rtt(), Some(10));
}
qos.user_network_delay(1, 122_000);
assert_eq!(qos.users[&1].delay.rtt_calculator.get_rtt(), Some(10));
for _ in 0..30 {
qos.user_delay_response_elapsed(1, 2500);
qos.user_network_delay(1, 2600);
}
assert_eq!(qos.users[&1].delay.rtt_calculator.get_rtt(), Some(10));
assert_eq!(qos.fps(), 5);
}
#[test]
fn stable_path_change_recovers_without_reconnecting() {
println!("| ABR | path delay | seconds to 30 FPS | final baseline | final FPS |");
println!("|---|---:|---:|---:|---:|");
for abr in [false, true] {
for delay in [310, 410, 500] {
let mut qos = super::smoke::session(FPS, Quality::Balanced);
qos.abr_config = abr;
qos.new_display("baseline".to_owned());
qos.set_support_changing_quality("baseline", true);
let second = |qos: &mut VideoQoS, delay| {
qos.advance_ms(1000);
let bitrate = (6000.0 * qos.ratio()) as u32;
qos.store_bitrate(bitrate);
qos.user_network_delay(1, delay);
let bitrate = (6000.0 * qos.ratio()) as u32;
qos.store_bitrate(bitrate);
qos.update_display_data("baseline", qos.fps() as usize);
};
for _ in 0..90 {
second(&mut qos, 10);
}
let mut first_recovered = None;
for s in 1..=90 {
second(&mut qos, delay);
if s >= 10 && qos.fps() == FPS && first_recovered.is_none() {
first_recovered = Some(s);
}
if s >= 40 {
assert_eq!(qos.fps(), FPS, "stay recovered: ABR={abr}, delay={delay}");
}
}
let base = qos.users[&1].delay.rtt_calculator.get_rtt().unwrap();
println!(
"| {abr} | 10 -> {delay} ms | {first_recovered:?} | {base} | {} |",
qos.fps()
);
assert!(first_recovered.is_some_and(|s| s <= 30));
assert_eq!(base, delay);
second(&mut qos, 10);
assert_eq!(qos.users[&1].delay.rtt_calculator.get_rtt(), Some(10));
}
}
}

View File

@@ -1,498 +0,0 @@
//! The controller's invariants as properties over random sessions. A scenario
//! test pins one trajectory; these hold whatever the trajectory:
//!
//! 1. viewer isolation: a viewer's private target is a function of its own
//! replies, timeouts and limit, never of another viewer's (with ABR on, the
//! shared bitrate state is the one designed input: the frame rate keeps a
//! floor while the bitrate can still come down);
//! 2. bad evidence never raises anything: a bad reply or a timeout tick keeps or
//! lowers that viewer's target and the bitrate ratio;
//! 3. lifecycle: a join adds a constraint and a leave removes it, and neither
//! touches any other viewer's state;
//! 4. evidence ownership: a bitrate cut is asked for by a viewer's own evidence,
//! by the step that viewer's own evidence calls for, and a newcomer's first
//! reply does not spend that evidence again;
//! 5. caps: a reply leaves the target within `[MIN_FPS, cap]`, and the stream is
//! the aggregation of the targets, the caps and the start-up guards;
//! 6. pairing: the late reply of a braked probe does not brake again.
use super::*;
/// xorshift64*, so the tests need no external crate and stay reproducible.
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng((seed ^ 0x9E37_79B9_7F4A_7C15).max(1))
}
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
fn chance(&mut self, pct: u64) -> bool {
self.below(100) < pct
}
}
#[derive(Debug, Clone, Copy)]
enum Step {
Reply { id: i32, delay: u32 },
Timeout { id: i32, elapsed: u128 },
Wait(u64),
Tick(usize),
Cap { id: i32, fps: u32 },
}
const SEEDS: u64 = 150;
const STEPS: usize = 300;
/// Random events for a set of viewers on one link. A probe that is out stays
/// out until a reply: the connection reports a growing elapsed time every second
/// and the reply that ends the stall carries at least that delay.
struct Driver {
rng: Rng,
ids: Vec<i32>,
base_rtt: u32,
outstanding: HashMap<i32, u128>,
}
impl Driver {
fn new(seed: u64, ids: Vec<i32>) -> Self {
let mut rng = Rng::new(seed);
let base_rtt = 10 + rng.below(300) as u32;
Driver {
rng,
ids,
base_rtt,
outstanding: HashMap::new(),
}
}
fn step(&mut self) -> Step {
let id = self.ids[self.rng.below(self.ids.len() as u64) as usize];
let roll = self.rng.below(100);
match roll {
0..=64 => {
let mut delay = if roll < 45 {
self.base_rtt + self.rng.below(140) as u32
} else {
self.base_rtt + DELAY_THRESHOLD_150MS + self.rng.below(1500) as u32
};
if let Some(elapsed) = self.outstanding.remove(&id) {
delay = delay.max(elapsed as u32 + self.rng.below(500) as u32);
}
Step::Reply { id, delay }
}
65..=74 => {
let elapsed = match self.outstanding.get(&id) {
Some(elapsed) => elapsed + 1000,
None => 2001 + self.rng.below(1000) as u128,
};
self.outstanding.insert(id, elapsed);
Step::Timeout { id, elapsed }
}
75..=89 => Step::Wait(self.rng.below(1500)),
90..=96 => Step::Tick(self.rng.below(31) as usize),
_ => Step::Cap {
id,
fps: 1 + self.rng.below(60) as u32,
},
}
}
}
/// What `on_connection_open` inserts, without touching the config store.
fn open(qos: &mut VideoQoS, id: i32) {
qos.users.insert(
id,
UserData {
joined_at: Some(qos.now()),
..Default::default()
},
);
}
fn session(abr: bool) -> VideoQoS {
let mut qos = VideoQoS::default();
qos.advance_ms(2000);
qos.abr_config = abr;
qos.first_reply_adjusts_ratio = true;
qos.new_display("test".to_owned());
qos.set_support_changing_quality("test", true);
qos.store_bitrate(4000);
qos
}
/// The video loop reports the encoder's bitrate as soon as it applies a ratio.
fn sync_bitrate(qos: &mut VideoQoS) {
let target = qos.latest_quality().ratio();
let ratio = qos.ratio();
qos.store_bitrate((4000.0 * ratio / target) as u32);
}
fn apply(qos: &mut VideoQoS, step: Step) {
match step {
Step::Reply { id, delay } => qos.user_network_delay(id, delay),
Step::Timeout { id, elapsed } => qos.user_delay_response_elapsed(id, elapsed),
Step::Wait(ms) => qos.advance_ms(ms),
Step::Tick(encoded) => qos.update_display_data("test", encoded),
Step::Cap { id, fps } => qos.user_custom_fps(id, fps),
}
sync_bitrate(qos);
}
/// The viewer's private target, as the controller reads it before a reply.
fn target(qos: &VideoQoS, id: i32) -> u32 {
let user = &qos.users[&id];
user.delay.fps.unwrap_or(INIT_FPS.min(user.fps_cap()))
}
fn baseline(qos: &VideoQoS, id: i32) -> Option<u32> {
qos.users[&id].delay.rtt_calculator.get_rtt()
}
/// Everything the controller keeps about a viewer, for change detection.
fn snapshot(qos: &VideoQoS, id: i32) -> String {
format!("{:?}", qos.users[&id])
}
/// The aggregation `adjust_fps` is meant to compute: the slowest viewer's target,
/// INIT_FPS for a viewer without a reply or inside its first second, within the
/// lowest cap.
fn expected_stream(qos: &VideoQoS) -> u32 {
let mut fps = qos
.users
.values()
.map(|u| u.delay.fps.unwrap_or(INIT_FPS))
.min()
.unwrap_or(INIT_FPS);
if qos
.users
.values()
.any(|u| u.joined_at.is_some_and(|j| qos.since(j).as_secs() < 1))
{
fps = fps.min(INIT_FPS);
}
let cap = qos
.users
.values()
.map(|u| u.fps_cap())
.min()
.unwrap_or(FPS);
fps.clamp(MIN_FPS, cap)
}
// Invariant 2: bad evidence never raises a target or the ratio.
#[test]
fn bad_evidence_never_raises_a_target_or_the_ratio() {
for seed in 0..SEEDS {
for abr in [false, true] {
let mut qos = session(abr);
let ids: Vec<i32> = (1..=1 + (seed % 3) as i32).collect();
for id in &ids {
open(&mut qos, *id);
}
let mut driver = Driver::new(seed, ids);
for step_no in 0..STEPS {
let step = driver.step();
let ratio_before = qos.ratio();
let before = match step {
Step::Reply { id, .. } | Step::Timeout { id, .. } => Some((id, target(&qos, id))),
_ => None,
};
apply(&mut qos, step);
// Bad by the baseline the controller used for this reply: the reply
// itself may have relearned it.
let bad = match step {
Step::Reply { id, delay } => baseline(&qos, id)
.is_some_and(|base| delay >= base + DELAY_THRESHOLD_150MS),
Step::Timeout { .. } => true,
_ => false,
};
if let (true, Some((id, before))) = (bad, before) {
let after = target(&qos, id);
assert!(
after <= before,
"seed {seed} abr {abr} step {step_no} {step:?}: target {before} -> {after}"
);
assert!(
qos.ratio() <= ratio_before,
"seed {seed} abr {abr} step {step_no} {step:?}: ratio {ratio_before} -> {}",
qos.ratio()
);
}
}
}
}
}
// Invariant 2 and 6: a timeout tick keeps or lowers the target, whatever it is.
#[test]
fn a_timeout_keeps_or_lowers_every_target() {
for reference in MIN_FPS..=MAX_FPS {
for elapsed in [2001, 2999, 3000, 3001, 4500, 6001, 9000, 30_000] {
let mut qos = session(false);
open(&mut qos, 1);
qos.user_custom_fps(1, MAX_FPS);
qos.users.get_mut(&1).unwrap().delay.fps = Some(reference);
qos.adjust_fps();
let stream = qos.fps();
qos.user_delay_response_elapsed(1, elapsed);
assert!(
target(&qos, 1) <= reference,
"{elapsed} ms outstanding at {reference} fps: target {}",
target(&qos, 1)
);
assert!(
qos.fps() <= stream,
"{elapsed} ms outstanding at {reference} fps: stream {stream} -> {}",
qos.fps()
);
}
}
}
// Invariant 6: the late reply of a braked probe does not brake again.
#[test]
fn a_late_reply_after_a_brake_does_not_brake_again() {
for reference in (MIN_FPS + 1..=MAX_FPS).step_by(3) {
for elapsed in [2001u32, 3001, 4500, 6001, 9000] {
for abr in [false, true] {
let mut qos = session(abr);
open(&mut qos, 1);
qos.user_custom_fps(1, MAX_FPS);
for _ in 0..3 {
qos.user_network_delay(1, 20);
}
qos.users.get_mut(&1).unwrap().delay.fps = Some(reference);
qos.user_delay_response_elapsed(1, elapsed as u128);
let braked = target(&qos, 1);
qos.user_network_delay(1, elapsed + 100);
assert_eq!(
target(&qos, 1),
braked,
"abr {abr}, {elapsed} ms outstanding at {reference} fps"
);
}
}
}
}
/// Viewer 1's target after each of its own events, alone or with company whose
/// events are interleaved: a second viewer with its own replies, timeouts, waits
/// and limits, and a third that joins and leaves along the way. The display
/// timer is left out: on a dynamic screen it raises the ratio off its floor,
/// and the property holds the bitrate state fixed.
fn viewer_one_targets(seed: u64, company: bool, abr: bool, at_floor: bool) -> Vec<u32> {
let mut own = Driver::new(seed, vec![1]);
let mut others = Driver::new(seed ^ 0xC0FF_EE, vec![2]);
let mut qos = session(abr);
if at_floor {
qos.ratio = qos.min_ratio();
sync_bitrate(&mut qos);
assert!(!qos.can_reduce_bitrate());
}
open(&mut qos, 1);
if company {
open(&mut qos, 2);
}
let no_tick = |step: Step| match step {
Step::Tick(_) => Step::Wait(1000),
step => step,
};
let mut targets = Vec::new();
for step_no in 0..STEPS {
if company {
for _ in 0..others.rng.below(3) {
let step = no_tick(others.step());
apply(&mut qos, step);
}
if step_no == STEPS / 3 {
open(&mut qos, 3);
others.ids.push(3);
}
if step_no == 2 * STEPS / 3 {
qos.on_connection_close(3);
others.ids.pop();
}
}
let step = no_tick(own.step());
apply(&mut qos, step);
targets.push(target(&qos, 1));
}
targets
}
// Invariant 1: another viewer's replies, timeouts, limits, joins and leaves do
// not change a viewer's private target. With ABR off the bitrate is fixed; with
// ABR on the shared bitrate state is a designed input, so the property is checked
// at the bitrate floor, where it can no longer change.
#[test]
fn a_viewers_target_is_independent_of_other_viewers() {
for seed in 0..SEEDS {
for (abr, at_floor) in [(false, false), (true, true)] {
let alone = viewer_one_targets(seed, false, abr, at_floor);
let with_company = viewer_one_targets(seed, true, abr, at_floor);
assert_eq!(
alone, with_company,
"seed {seed} abr {abr}: viewer 1's targets differ with company"
);
}
}
}
// Invariant 3: a join adds a constraint, a leave removes it, and neither touches
// another viewer's state.
#[test]
fn joins_and_leaves_only_change_the_aggregation() {
for seed in 0..SEEDS {
for abr in [false, true] {
let mut qos = session(abr);
open(&mut qos, 1);
open(&mut qos, 2);
let mut driver = Driver::new(seed, vec![1, 2]);
let mut next_id = 3;
let mut present: Vec<i32> = Vec::new();
for step_no in 0..STEPS {
let step = driver.step();
apply(&mut qos, step);
if driver.rng.chance(10) {
let others: Vec<i32> = qos.users.keys().copied().collect();
let before: Vec<String> = others.iter().map(|id| snapshot(&qos, *id)).collect();
qos.adjust_fps();
let stream = qos.fps();
open(&mut qos, next_id);
present.push(next_id);
driver.ids.push(next_id);
next_id += 1;
qos.adjust_fps();
assert!(
qos.fps() <= stream,
"seed {seed} abr {abr} step {step_no}: a join raised the stream {stream} -> {}",
qos.fps()
);
assert_eq!(qos.fps(), expected_stream(&qos));
let after: Vec<String> = others.iter().map(|id| snapshot(&qos, *id)).collect();
assert_eq!(
before, after,
"seed {seed} abr {abr} step {step_no}: a join changed a viewer"
);
} else if !present.is_empty() && driver.rng.chance(10) {
let leaving = present.remove(driver.rng.below(present.len() as u64) as usize);
driver.ids.retain(|id| *id != leaving);
driver.outstanding.remove(&leaving);
let others: Vec<i32> = qos.users.keys().copied().filter(|id| *id != leaving).collect();
let before: Vec<String> = others.iter().map(|id| snapshot(&qos, *id)).collect();
qos.on_connection_close(leaving);
let after: Vec<String> = others.iter().map(|id| snapshot(&qos, *id)).collect();
assert_eq!(
before, after,
"seed {seed} abr {abr} step {step_no}: a leave changed a viewer"
);
assert_eq!(
qos.fps(),
expected_stream(&qos),
"seed {seed} abr {abr} step {step_no}: the stream after a leave"
);
}
}
}
}
}
// Invariant 4: the ratio comes down only when a viewer's own evidence asks for
// it, by that viewer's own step, and a newcomer's first reply does not spend the
// evidence again.
#[test]
fn a_bitrate_cut_is_owned_by_a_viewers_evidence() {
let mut cuts = 0;
for seed in 0..SEEDS {
let mut qos = session(true);
let ids: Vec<i32> = (1..=1 + (seed % 3) as i32).collect();
for id in &ids {
open(&mut qos, *id);
}
let mut driver = Driver::new(seed, ids);
for step_no in 0..STEPS {
let step = driver.step();
let before = qos.ratio();
apply(&mut qos, step);
let after = qos.ratio();
if after >= before {
continue;
}
cuts += 1;
let asked: Vec<f32> = qos
.users
.values()
.filter_map(|u| u.delay.ratio_reduction())
.collect();
assert!(
!asked.is_empty(),
"seed {seed} step {step_no} {step:?}: a cut nobody asked for"
);
let deepest = asked.iter().copied().fold(1.0_f32, f32::min);
assert!(
after >= before * deepest * 0.999,
"seed {seed} step {step_no} {step:?}: cut {before} -> {after}, deepest step asked {deepest}"
);
// A newcomer replying inside the cooldown finds the evidence spent.
open(&mut qos, 99);
qos.advance_ms(driver.rng.below(2900));
qos.user_network_delay(99, driver.base_rtt);
assert_eq!(
qos.ratio(),
after,
"seed {seed} step {step_no}: a newcomer's first reply spent the evidence again"
);
qos.on_connection_close(99);
}
}
assert!(cuts > SEEDS as usize, "only {cuts} cuts across {SEEDS} sessions");
}
// Invariant 5: a reply leaves the target within its cap, and the stream is the
// aggregation of targets, caps and start-up guards after every decision.
#[test]
fn targets_stay_within_caps_and_the_stream_is_their_aggregation() {
for seed in 0..SEEDS {
for abr in [false, true] {
let mut qos = session(abr);
let ids: Vec<i32> = (1..=1 + (seed % 3) as i32).collect();
for id in &ids {
open(&mut qos, *id);
}
let mut driver = Driver::new(seed, ids);
for step_no in 0..STEPS {
let step = driver.step();
apply(&mut qos, step);
match step {
Step::Reply { id, .. } => {
let cap = qos.users[&id].fps_cap();
let t = target(&qos, id);
assert!(
(MIN_FPS..=cap).contains(&t),
"seed {seed} abr {abr} step {step_no} {step:?}: target {t} outside [{MIN_FPS}, {cap}]"
);
}
Step::Wait(_) | Step::Cap { .. } => continue,
_ => {}
}
assert_eq!(
qos.fps(),
expected_stream(&qos),
"seed {seed} abr {abr} step {step_no} {step:?}: the stream is not the aggregation"
);
}
}
}
}

View File

@@ -1,493 +0,0 @@
use super::*;
fn abr_session() -> VideoQoS {
let mut qos = stable_qos();
qos.new_display("test".to_owned());
qos.set_support_changing_quality("test", true);
qos.store_bitrate(4000);
// Linux skips the first-reply adjustment; exercise it on every platform.
qos.first_reply_adjusts_ratio = true;
qos.advance_ms(4000);
qos
}
#[test]
fn bitrate_reduction_precedes_ordinary_fps_reduction() {
let mut qos = abr_session();
let ratio = qos.ratio();
qos.user_network_delay(1, 400);
assert_eq!(qos.fps(), FPS);
assert_eq!(qos.ratio(), ratio);
qos.user_network_delay(1, 400);
assert_eq!(qos.fps(), FPS);
assert!(qos.ratio() < ratio);
qos.user_network_delay(1, 400);
assert_eq!(qos.fps(), FPS);
qos.user_network_delay(1, 400);
assert!(qos.fps() < FPS);
}
#[test]
fn bitrate_cooldown_defers_ordinary_fps_reduction() {
let mut qos = abr_session();
qos.adjust_ratio_instant = qos.now();
let ratio = qos.ratio();
for _ in 0..3 {
qos.user_network_delay(1, 400);
assert_eq!(qos.fps(), FPS);
assert_eq!(qos.ratio(), ratio);
}
qos.advance_ms(4000);
qos.user_network_delay(1, 400);
assert_eq!(qos.fps(), FPS);
assert!(qos.ratio() < ratio);
qos.user_network_delay(1, 400);
assert_eq!(qos.fps(), FPS);
qos.user_network_delay(1, 400);
assert!(qos.fps() < FPS);
}
#[test]
fn unavailable_abr_or_minimum_bitrate_does_not_prevent_fps_reduction() {
for mode in ["disabled", "unsupported", "minimum"] {
let mut qos = abr_session();
match mode {
"disabled" => qos.abr_config = false,
"unsupported" => qos.set_support_changing_quality("test", false),
"minimum" => qos.ratio = BR_MIN_HIGH_RESOLUTION,
_ => unreachable!(),
}
for _ in 0..3 {
qos.user_network_delay(1, 400);
}
assert!(qos.fps() < FPS, "{mode}");
}
}
#[test]
fn severe_delay_and_timeout_bypass_bitrate_cooldown() {
let mut qos = abr_session();
qos.adjust_ratio_instant = qos.now();
qos.user_network_delay(1, 1200);
assert_eq!(qos.fps(), 15);
qos.user_delay_response_elapsed(1, 2500);
assert_eq!(qos.fps(), 7);
}
#[test]
fn minimum_bitrate_during_cooldown_does_not_block_fps_reduction() {
// ABR on, ratio at its floor, a good reply cleared the post-reduction counter,
// and the adjustment cooldown has just restarted: bitrate cannot help here.
let mut qos = abr_session();
qos.ratio = BR_MIN_HIGH_RESOLUTION;
qos.user_network_delay(1, 10);
qos.adjust_ratio_instant = qos.now();
for _ in 0..3 {
qos.user_network_delay(1, 400);
}
assert!(qos.fps() < FPS);
}
fn abr_session_from_scratch() -> VideoQoS {
let mut qos = VideoQoS::default();
qos.advance_ms(2000);
qos.users.insert(1, UserData::default());
qos.new_display("test".to_owned());
qos.set_support_changing_quality("test", true);
qos.store_bitrate(4000);
qos
}
/// The video loop reports the encoder's bitrate as soon as it applies a new ratio.
fn sync_bitrate(qos: &mut VideoQoS) {
let target = qos.latest_quality().ratio();
let ratio = qos.ratio();
qos.store_bitrate((4000.0 * ratio / target) as u32);
}
/// One second of wall clock, one probe reply, one display update: what a
/// connection does every second.
fn second(qos: &mut VideoQoS, delay: u32, encoded: usize) {
qos.advance_ms(1000);
sync_bitrate(qos);
qos.user_network_delay(1, delay);
sync_bitrate(qos);
qos.update_display_data("test", encoded);
sync_bitrate(qos);
}
#[test]
fn stable_high_rtt_restores_bitrate() {
for rtt in [180, 300] {
let mut qos = abr_session_from_scratch();
let target = qos.latest_quality().ratio();
for _ in 0..120 {
second(&mut qos, rtt, 30);
}
assert_eq!(qos.fps(), FPS, "rtt {rtt}");
assert!(
qos.ratio() >= target * 0.99,
"rtt {rtt}: ratio {}",
qos.ratio()
);
}
}
#[test]
fn congestion_bitrate_reduction_resets_dynamic_screen_window() {
// A static screen encodes about one frame per second. While the congestion path
// adjusts the ratio at every cooldown, the periodic branch never runs, so the
// encode counter must not keep accumulating across the whole episode.
let mut qos = abr_session();
for _ in 0..10 {
qos.advance_ms(4000);
qos.user_network_delay(1, 10);
qos.user_network_delay(1, 400);
qos.user_network_delay(1, 400);
qos.update_display_data("test", 1);
}
for _ in 0..12 {
qos.advance_ms(1000);
qos.user_network_delay(1, 10);
}
let ratio = qos.ratio();
qos.advance_ms(4000);
qos.update_display_data("test", 1);
assert!(
qos.ratio() <= ratio,
"a static screen must not look dynamic after congestion"
);
}
#[test]
fn a_single_stall_does_not_cut_bitrate() {
// One probe out for 2.5 s, then its late reply: jitter, not congestion.
let mut qos = abr_session();
let ratio = qos.ratio();
qos.user_delay_response_elapsed(1, 2500);
qos.advance_ms(1000);
qos.update_display_data("test", 30);
assert_eq!(qos.ratio(), ratio, "the timeout tick alone");
qos.user_network_delay(1, 2600);
assert_eq!(qos.ratio(), ratio, "the late reply alone");
qos.user_network_delay(1, 400);
assert!(qos.ratio() < ratio, "a second bad reply confirms");
}
#[test]
fn a_stall_beyond_three_seconds_cuts_bitrate() {
let mut qos = abr_session();
let ratio = qos.ratio();
qos.user_delay_response_elapsed(1, 2001);
qos.update_display_data("test", 30);
assert_eq!(qos.ratio(), ratio);
qos.advance_ms(1000);
qos.user_delay_response_elapsed(1, 3001);
qos.update_display_data("test", 30);
assert!(qos.ratio() < ratio, "still out at the next tick");
}
#[test]
fn stable_high_rtt_does_not_dip_at_start() {
for rtt in [180, 300] {
let mut qos = super::smoke::session(30, Quality::Balanced);
for _ in 0..20 {
qos.advance_ms(1000);
qos.user_network_delay(1, rtt);
assert!(qos.fps() >= INIT_FPS, "rtt {rtt}: {}", qos.fps());
}
assert_eq!(qos.fps(), FPS, "rtt {rtt}");
}
}
#[test]
fn confirmed_severe_congestion_halves_bitrate() {
let mut qos = abr_session();
let ratio = qos.ratio();
qos.user_network_delay(1, 800);
qos.user_network_delay(1, 800); // two bad replies: an ordinary step
let after_first = qos.ratio();
assert!(
after_first < ratio && after_first > ratio * 0.75,
"{after_first}"
);
qos.user_network_delay(1, 800); // three: confirmed
qos.advance_ms(4000);
qos.update_display_data("test", 30);
assert!(qos.ratio() <= after_first * 0.55, "{}", qos.ratio());
}
#[test]
fn fps_holds_its_floor_while_bitrate_can_still_drop() {
// With a bitrate-targeted encoder fewer frames do not mean fewer bytes, so the
// bitrate comes down first and the frame rate keeps its floor meanwhile.
let mut qos = abr_session();
let mut reached_floor = false;
// Keep the queue growing, rather than presenting a stable new path delay.
let mut delay = 400;
for _ in 0..60 {
qos.advance_ms(3000);
second(&mut qos, delay, 30);
delay += 10;
if qos.ratio() > 0.17 {
assert!(
qos.fps() >= 10,
"fps {} at ratio {}",
qos.fps(),
qos.ratio()
);
} else {
reached_floor = true;
break;
}
}
assert!(
reached_floor,
"bitrate must reach its floor: {}",
qos.ratio()
);
for _ in 0..24 {
qos.user_network_delay(1, delay);
delay += 10;
}
assert!(
qos.fps() < 10,
"an exhausted bitrate frees the frame rate: {}",
qos.fps()
);
}
#[test]
fn bitrate_timer_does_not_punish_unconfirmed_spikes_or_stale_averages() {
let mut qos = abr_session();
let ratio = qos.ratio();
for delay in [800, 10, 350, 10, 350, 10].repeat(10) {
qos.user_network_delay(1, delay);
qos.adjust_ratio(false);
assert_eq!(qos.fps(), FPS);
assert_eq!(qos.ratio(), ratio);
}
}
#[test]
fn viewers_confirm_congestion_independently() {
let mut qos = stable_qos();
qos.users.insert(2, UserData::default());
for _ in 0..30 {
qos.user_network_delay(2, 10);
qos.user_network_delay(1, 10);
}
for id in [1, 2, 1, 2] {
qos.user_network_delay(id, 400);
assert_eq!(qos.fps(), FPS);
}
qos.user_network_delay(2, 10);
qos.user_network_delay(1, 400);
assert!(qos.fps() < FPS);
qos.user_custom_fps(2, 12);
qos.user_network_delay(1, 10);
assert_eq!(qos.fps(), 12);
}
#[test]
fn a_congested_viewer_does_not_lower_another_viewers_target() {
let mut qos = stable_qos();
qos.users.insert(2, UserData::default());
for _ in 0..30 {
qos.user_network_delay(2, 10);
qos.user_network_delay(1, 10);
}
assert_eq!(qos.fps(), FPS);
// Viewer 1 congests; the stream follows the slowest viewer.
for _ in 0..2 {
qos.user_network_delay(1, 1200);
}
assert_eq!(qos.fps(), 8);
// Viewer 2 is fine and keeps its own target rather than inheriting viewer 1's.
qos.user_network_delay(2, 10);
assert_eq!(qos.users[&2].delay.fps, Some(FPS));
// Once viewer 1 restores, the stream is back at once.
for _ in 0..3 {
qos.user_network_delay(1, 10);
}
assert_eq!(qos.fps(), FPS);
}
#[test]
fn pending_probe_checks_do_not_count_as_fresh_bad_replies() {
let mut qos = stable_qos();
qos.user_network_delay(1, 400);
for elapsed in [1000, 1500, 1900] {
qos.user_delay_response_elapsed(1, elapsed);
assert_eq!(qos.fps(), FPS);
}
qos.user_network_delay(1, 400);
assert_eq!(qos.fps(), FPS);
qos.user_network_delay(1, 10);
qos.user_network_delay(1, 400);
qos.user_network_delay(1, 400);
assert_eq!(qos.fps(), FPS);
}
#[test]
fn recovery_continues_with_intermittent_jitter() {
let mut qos = stable_qos();
for _ in 0..3 {
qos.user_network_delay(1, 1200);
}
assert_eq!(qos.fps(), 5);
for delay in [10, 350].repeat(30) {
qos.user_network_delay(1, delay);
}
assert_eq!(qos.fps(), FPS);
}
#[test]
fn custom_limit_of_one_viewer_does_not_lower_another_viewers_target() {
let mut qos = stable_qos();
qos.users.insert(2, UserData::default());
for _ in 0..30 {
qos.user_network_delay(2, 10);
qos.user_network_delay(1, 10);
}
qos.user_custom_fps(2, 12);
qos.user_network_delay(1, 10);
assert_eq!(qos.fps(), 12, "the stream follows the lowest limit");
assert_eq!(
qos.users[&1].delay.fps,
Some(FPS),
"viewer 1's own target is not a function of viewer 2's limit"
);
qos.on_connection_close(2);
assert_eq!(
qos.fps(),
FPS,
"the stream is back the moment the limit is gone"
);
}
#[test]
fn new_viewers_first_reply_does_not_bypass_bitrate_cooldown() {
let mut qos = abr_session();
for _ in 0..3 {
qos.user_network_delay(1, 800);
}
// Viewer 1 is confirmed and its evidence was spent on a cut a moment ago.
let ratio = qos.ratio();
assert!(ratio < Quality::Balanced.ratio());
qos.users.insert(2, UserData::default());
qos.user_network_delay(2, 10);
assert_eq!(
qos.ratio(),
ratio,
"viewer 2's first reply must not spend viewer 1's evidence again inside the cooldown"
);
}
#[test]
fn new_viewer_does_not_inherit_another_viewers_congested_fps() {
let mut qos = stable_qos();
for _ in 0..2 {
qos.user_network_delay(1, 1200);
}
assert_eq!(qos.fps(), 8);
qos.users.insert(2, UserData::default());
qos.user_network_delay(2, 10);
assert!(
qos.users[&2].delay.fps >= Some(INIT_FPS),
"a new viewer starts from INIT_FPS, not from the congested stream: {:?}",
qos.users[&2].delay.fps
);
}
#[test]
fn unconfirmed_severe_viewer_does_not_amplify_another_viewers_confirmed_mild_congestion() {
let mut qos = abr_session();
qos.users.insert(2, UserData::default());
for _ in 0..30 {
qos.user_network_delay(2, 10);
qos.user_network_delay(1, 10);
}
// The first reply of a new viewer adjusts the ratio and restarts the cooldown.
qos.advance_ms(4000);
let ratio = qos.ratio();
// Viewer 2: mild congestion, confirmed over three replies. Viewer 1: one
// severe spike, never confirmed. Each viewer on its own calls for at most a
// five percent step; together they must not turn into a halving.
qos.user_network_delay(2, 200);
qos.user_network_delay(1, 1200);
qos.user_network_delay(2, 200);
let after_two = qos.ratio();
assert!(after_two < ratio, "viewer 2's second bad reply cuts");
assert!(
after_two >= ratio * 0.94,
"viewer 2's own mild excess is a five percent step, not {after_two}"
);
qos.user_network_delay(2, 200);
qos.advance_ms(4000);
qos.update_display_data("test", 30);
assert!(
qos.ratio() >= after_two * 0.94,
"viewer 1's severity must not be paired with viewer 2's confirmation: {}",
qos.ratio()
);
}
/// What `on_connection_open` inserts, without touching the config store.
fn newcomer(qos: &VideoQoS) -> UserData {
UserData {
joined_at: Some(qos.now()),
..Default::default()
}
}
#[test]
fn closing_a_just_opened_viewer_does_not_throttle_existing_viewers() {
let mut qos = stable_qos();
assert_eq!(qos.fps(), FPS);
qos.users.insert(2, newcomer(&qos));
assert_eq!(
qos.fps(),
FPS,
"nothing changes until the stream is re-aggregated"
);
qos.on_connection_close(2);
assert_eq!(
qos.fps(),
FPS,
"the guard leaves with the viewer that brought it"
);
}
#[test]
fn a_new_viewer_caps_the_stream_at_init_fps_for_a_second() {
let mut qos = stable_qos();
qos.users.insert(2, newcomer(&qos));
qos.user_network_delay(1, 10);
assert_eq!(qos.fps(), INIT_FPS);
qos.advance_ms(1000);
qos.user_network_delay(2, 10);
qos.user_network_delay(1, 10);
assert!(
qos.fps() > INIT_FPS,
"after a second the stream follows the viewers' own targets: {}",
qos.fps()
);
}
#[test]
fn closing_the_latest_newcomer_keeps_an_earlier_newcomers_guard() {
let mut qos = stable_qos();
qos.users.insert(2, newcomer(&qos));
qos.user_network_delay(2, 10);
assert_eq!(qos.fps(), INIT_FPS);
qos.advance_ms(100);
qos.users.insert(3, newcomer(&qos));
qos.advance_ms(100);
qos.on_connection_close(3);
assert_eq!(
qos.fps(),
INIT_FPS,
"viewer 2 is still inside its own start-up window"
);
}

View File

@@ -1,120 +0,0 @@
use super::*;
#[test]
fn ordinary_congestion_waits_between_bounded_cuts() {
for delay in [400, 800] {
let mut qos = stable_qos();
for _ in 0..2 {
qos.user_network_delay(1, delay);
assert_eq!(qos.fps(), FPS);
}
qos.user_network_delay(1, delay);
let first_cut = qos.fps();
assert!((24..FPS).contains(&first_cut), "delay={delay}: {first_cut}");
for _ in 0..2 {
qos.user_network_delay(1, delay);
assert_eq!(qos.fps(), first_cut, "wait for new evidence after a cut");
}
qos.user_network_delay(1, delay);
assert!(qos.fps() < first_cut);
assert!(qos.fps() >= first_cut - first_cut / 5);
}
}
#[test]
fn automatic_floor_preserves_lower_custom_limits() {
for limit in [1, 3, 5, 30, 60, 120] {
for abr in [false, true] {
for timeout in [false, true] {
let mut qos = super::smoke::session(limit, Quality::Balanced);
qos.abr_config = abr;
qos.new_display("test".to_owned());
qos.set_support_changing_quality("test", true);
for _ in 0..90 {
qos.user_network_delay(1, 10);
}
assert_eq!(qos.fps(), limit);
qos.ratio = BR_MIN_HIGH_RESOLUTION;
qos.store_bitrate(600);
let floor = 5.min(limit);
if timeout {
for elapsed in [2001, 3001, 4001, 5001, 10_000, 30_000] {
qos.user_delay_response_elapsed(1, elapsed);
assert!(
(floor..=limit).contains(&qos.fps()),
"timeout={elapsed}, limit={limit}, ABR={abr}"
);
}
} else {
for _ in 0..8 {
qos.user_network_delay(1, 1500);
assert!(
(floor..=limit).contains(&qos.fps()),
"limit={limit}, ABR={abr}"
);
}
}
assert_eq!(
qos.fps(),
floor,
"timeout={timeout}, limit={limit}, ABR={abr}"
);
if timeout {
qos.user_network_delay(1, 30_100);
assert_eq!(qos.fps(), floor, "a late reply must preserve the floor");
}
for _ in 0..2 {
qos.user_network_delay(1, 10);
}
assert_eq!(qos.fps(), limit, "recover: timeout={timeout}, ABR={abr}");
}
}
}
}
#[test]
fn two_good_replies_restore_after_a_severe_stall() {
let mut qos = stable_qos();
qos.user_delay_response_elapsed(1, 5001);
assert_eq!(qos.fps(), 5);
qos.user_network_delay(1, 5100);
assert_eq!(qos.fps(), 5, "the late reply must not brake twice");
qos.user_network_delay(1, 10);
assert!(
(5..FPS).contains(&qos.fps()),
"one good reply is not enough"
);
qos.user_network_delay(1, 10);
assert_eq!(qos.fps(), FPS);
}
#[test]
fn jitter_during_recovery_does_not_discard_the_restore_target() {
let mut qos = stable_qos();
for _ in 0..3 {
qos.user_network_delay(1, 1200);
}
for delay in [10, 350, 10] {
qos.user_network_delay(1, delay);
}
assert_eq!(qos.fps(), FPS);
}
#[test]
fn a_failed_fast_restore_rolls_back_before_the_queue_grows() {
let mut qos = stable_qos();
qos.user_delay_response_elapsed(1, 5001);
for _ in 0..2 {
qos.user_network_delay(1, 10);
}
assert_eq!(qos.fps(), FPS);
qos.user_network_delay(1, 800);
assert_eq!(qos.fps(), FPS / 2);
for _ in 0..2 {
qos.user_network_delay(1, 10);
}
assert!(
qos.fps() < FPS,
"a failed restore must lower the next probe"
);
}

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