Compare commits

..

51 Commits

Author SHA1 Message Date
rustdesk
1efedacafb server: bound unauthenticated connections in number and in time
A connection that never logs in costs whatever its transport costs, for as
long as it keeps itself alive: the only limit was the 30s idle timeout, which
any message resets. Nothing bounded how many such connections one machine
holds, on any transport. The shape sshd_config answers with LoginGraceTime
and MaxStartups.

Every connection is admitted among the unauthorized ones before its identity
handshake, in create_tcp_connection, and holds that place until it
authorizes or ends: the count of live places is the bound, not a ledger
beside the connections: the resource bound. One address may hold sixteen, a
quarter of the room; a further connection from it is refused before the
handshake. That share is a fairness cap against the cheapest flood, one host
with one address, not a security boundary: any pool of addresses passes it,
and the global limit is what holds. With 64 held in all, a further arrival
is refused too, and the oldest connection is told to go, unless one is on
its way out already: the handshake is raced against that eviction and ends
at once, and the session loop has it as a branch of its select, so the place
opens as soon as the connection has actually gone and not on a timer tick.
The newcomer is not let in on a place still occupied; the controller retries
on its own with backoff, and by then the place is free. At most one
connection is ever on its way out, so a burst of refused arrivals clears no
more room than a single one, and the retry that takes the freed place counts
against its address's share: one address turns out at most as many
connections as it may hold.

One deadline, from the moment the connection starts, a branch of the session
loop's select rather than a check on the TestDelay tick: a connection not
authorized after 180s is closed, however alive it keeps itself, a wrong
password, a pending 2FA, an accept prompt or an admin-terminal credential
prompt left unanswered. The controller reconnects on its own and the prompt
comes back. It closes with the Timeout reason the idle path uses, and that
path still ends a connection that says nothing for 30s. There is no shorter
deadline for the first login request: an admin-terminal controller shows
its credential prompt before sending one, and a peer that wanted to dodge
such a deadline would only have to send a login request, so it would bound
nothing.

The peer address is normalized with try_into_v4 before admission, the same
form Connection::start keys the whitelist on, so an IPv4 peer and its
IPv4-mapped IPv6 form are one address and not two shares.

The WebRTC answerer's slot keeps bounding peer connection setup up to the
open data channel; from there this covers it like every other transport.

Tests cover the registry and the live bound: an address over its share is
refused while others are admitted; at the limit the newcomer is refused, the
oldest is told to go, nobody else is while it is on its way out, and its
place frees only when it has; an address at the limit turns out no more
connections than its share and is then refused without evicting anyone; and
with the limit held by 64 connections stalled in the handshake, one more
arrival is refused while the oldest handshake ends at once and only then is
there a place again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
2026-09-16 15:45:51 +08:00
rustdesk
4f56d6957a pin the webrtc crate to the fork that exposes max_binding_requests
The hbb_common bump before this one calls SettingEngine::set_ice_max_binding_requests(),
which the 0.13.0 release on crates.io does not have: only webrtc-util and webrtc-sctp
were patched to the fork, so the webrtc crate itself still came from the registry and
the build stopped at that call.

The three patches now point at the same fork revision, one commit past the one they
were on, which adds the setter. The webrtc crate depends on its siblings by path, so
patching it moves the rest of that workspace to the fork as well; the fork is upstream
v0.13.0 with changes to sctp and to this setter only, so those crates carry the same
code they did from the registry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-16 14:18:03 +08:00
rustdesk
f312b57909 bump hbb_common: cap the answerer's ICE binding requests at 74
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-16 13:28:32 +08:00
Joss Gray
f9c2f16857 fix: recover DXGI capture after access loss (#16024)
* fix: recover DXGI capture after access loss

* fix: track DXGI frame ownership

* fix: only recover DXGI access loss

* fix: stabilize DXGI recovery after mode switches

* fix: preserve DXGI recovery budget

* refactor: model DXGI frame lifecycle as enum

* docs: clarify DXGI frame cleanup order

* fix: scope DXGI frame grace to access loss recovery

* fix: preserve DXGI recovery across display changes

* chore: log DXGI recovery attempts at debug level
2026-09-16 13:18:44 +08:00
vipe3
8154c026a4 fix(android): default to entire screen for capture (#16163)
Use MediaProjectionConfig on Android 14+ to remove the app-versus-screen choice while retaining the legacy fallback.
2026-09-16 12:01:26 +08:00
fufesou
28269af9c7 fix(audio): preserve compatible playback and recover failed Windows outputs (#16150)
* fix(audio): preserve compatible playback on replacement failure

* fix(audio): use Windows events for capture worker wakeups

* fix(audio): recover Windows playback after output device failure

* reduce diffs

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): reset decoder when retaining compatible playback

* fix(audio): integrate playback retention and Windows capture wakeups

* revert(audio): remove Windows capture-event notification changes

* docs(audio): explain Windows 7 capture limitations

* docs(audio): clarify capture changes introduced by #16095

* fix(audio): preserve playback through asynchronous Windows startup

Keep a compatible active output until the replacement callback confirms startup, preserving decoder progress on promotion or rollback. Handle superseding formats and simultaneous output failures without losing recovery state. Pin the scoped CPAL WASAPI event-ownership fix and add deterministic and native regressions.

* fix(audio): retain ready output across superseding formats

* fix(audio): update CPAL teardown recovery

Pin the upstream-aligned WASAPI cleanup with shared event lifetime, self-join prevention and fallible destructor diagnostics. Preserve the existing dependency graph and audio implementation.

* Use upstream-style CPAL stream teardown

* Refact: remove low value test

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): retry playback when startup confirmation times out

Signed-off-by: fufesou <linlong1266@gmail.com>

* update cpal

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-16 11:55:53 +08:00
Maison da Silva
20e25e743d Fix Portuguese translations in ptbr.rs (#16234)
Fix Portuguese translations in ptbr.rs
2026-09-16 11:00:49 +08:00
RustDesk
851d2df88c fix(audio): three 100% CPU busy loops on the _pa path (#16229)
The Linux audio service in `--server` ignored the `Err` from `next_raw()`, so
once the cm-side `_pa` peer closed, every iteration re-polled a dead socket:
tokio-util's paused `Framed` issues one 0-byte read per poll and returns ready
at once, never `Pending`. The thread never parked and burned a full core for
the life of the process. Propagate instead, so `ServiceTmpl::run`'s existing
backoff ends the inner loop and reconnects.

Two sibling loops on the same audio path have the same shape:

- `ipc::start_pa` (runs in `--cm`) ignored the `Err` from
  `psimple::Simple::read`, so a dead pulse handle spins there instead.
- `start_voice_call`'s forwarding thread polls two channels with `try_recv`
  and has no blocking primitive at all: measured 99.8% of a core for the whole
  call, against 1.0% with a 1 ms pause (audio packets arrive every 10 ms).

fix https://github.com/rustdesk/rustdesk/issues/16226

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:35:40 +08:00
solokot
be757de462 Update ru.rs (#16217) 2026-09-15 16:14:49 +08:00
fufesou
a437eb9bc6 fix(windows): recognize Hyper-V Enhanced Sessions by protocol (#16223)
Fixes #16182

Use WTSClientProtocolType to identify RDP sessions with nonstandard
names during session selection and enumeration.

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-15 15:21:19 +08:00
21pages
39d4f1854b chore(qos): remove verbose diagnostics while retaining trace logs (#16214) 2026-09-14 17:19:58 +08:00
VenusGirl❤
bd028ad350 Update Korean (#16212) 2026-09-14 16:15:55 +08:00
bovirus
b48fe17b52 Update Italian language (#16211)
* Update it.rs

* Update it.rs
2026-09-14 16:15:31 +08:00
rustdesk
82b6fc2f3b https://github.com/rustdesk/rustdesk/pull/16199/ 2026-09-14 14:38:55 +08:00
rustdesk
29772f6595 fix https://github.com/rustdesk/rustdesk/issues/16208 2026-09-14 14:06:03 +08:00
Kauan Kelvin
d0ee56d349 fastlane: add it-IT Android metadata (#16162)
Signed-off-by: Kauan Kelvin <kelvinkauan722@gmail.com>
2026-09-14 12:44:31 +08:00
VenusGirl❤
4515cd6463 Update Korean (#16197)
* Update Korean

* Update src/lang/ko.rs

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update src/lang/ko.rs

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-09-14 12:43:50 +08:00
fufesou
9b5f342839 fix(audio): throttled debug logs (#16172)
* fix(audio): throttled debug logs

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): preserve contention counts across throttled reports

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-14 12:26:23 +08:00
fufesou
cdcb4d4b4c Fix/android voice call (#16180)
* fix(android): handle missing audio permission in voice calls

- Avoid stop errors when voice capture was never started
- Dispatch voice call error dialogs on the main thread
- Explain how to enable Audio capture on the Screen share page

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(android): restore audio after failed voice call switches

* fix: translations

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: translatin

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(i18n): match UI label capitalization in translated messages

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-09-14 12:24:33 +08:00
21pages
34c340be4e Upgrade FFmpeg to 7.1.1 to fix HEVC WPP deadlocks (#16169)
FFmpeg 7.1 can deadlock during software HEVC decoding with WPP slice
threading, as reproduced on Linux and macOS. Version 7.1.1 includes the
upstream progress2 fix (79c47dfd25f101b6842bbec8c6ffef8d5077c3ae).

Update the overlay version and archive checksum, reset the port revision,
and document the fix. Existing FFmpeg patches and build options are
unchanged, and decoding can retain its existing thread-count policy.

Validation on macOS arm64:
- Built the overlay successfully with all 23 existing patches.
- HEVC four-thread replay: 1,000 rounds / 71,000 frames without a stall;
  resolution changes: 6,816 frames; H.264 replay: 4,100 frames.
- VideoToolbox H.264/HEVC encoding with software and hardware decoding:
  all six cases matched the FFmpeg 7.1 baseline.
- git diff --check and manifest/archive checksum validation passed.
2026-09-14 10:43:59 +08:00
bovirus
9f60bb6fc5 Update it.rs (#16171) 2026-09-14 10:43:14 +08:00
Maison da Silva
5d7e20103a Translate terminal clipboard tips to Portuguese (#16183)
* Translate terminal clipboard tips to Portuguese

* Update terminal clipboard write tip translation
2026-09-14 10:42:36 +08:00
Vojtěch Lapuník
e4b9594c5e fix(android): exclude android from fixed 64 audio buffer size (#16198) 2026-09-14 10:40:18 +08:00
Elyor
8b716c7046 flutter: fix End connetion typo (#16202)
Co-authored-by: Elyor1977 <elyor77q@gmail.com>
2026-09-14 10:23:33 +08:00
Marc Frank
70d45051f6 update base image in Dockerfile (#16176)
Debian Bullseye has reached end‑of‑life and is no longer supported.

Signed-off-by: Marc Frank <78813606+D-MarcFrank@users.noreply.github.com>
2026-09-14 09:36:30 +08:00
fufesou
bf1ebe5be2 Ci/native arm64 msbuild (#16170)
* ci: use native ARM64 MSBuild for MSI packaging

* ci: clarify MSBuild host and MSI target architectures
2026-09-12 14:31:16 +08:00
rustdesk
3c7c13d79d timeout of webrtc fallback to delay 2026-09-12 13:52:11 +08:00
fufesou
e54f21e10c Fix/ci (#16168)
* fix(ci): allow native x86 Rust toolchain on Windows

* docs(ci): explain the rustup 1.29.1 host check

* docs(ci): explain the unverified x64 cross-compilation alternative
2026-09-12 11:33:49 +08:00
21pages
e82dd12350 Lower QoS sample logging to trace (#16161) 2026-09-11 18:24:34 +08:00
rustdesk
d5311574be fix(flutter): show the quality monitor's Transport row on the web only
The row was added for the web client, which has no session tab to name
the transport on, but nothing gated it: a desktop session over WebRTC
showed it too, duplicating the tab tooltip's "(WebRTC)". The getter now
answers only on the web, as its own comment intended.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcJZZeJ3Nqb2MHxXuUadkb
2026-09-11 15:04:31 +08:00
Maison da Silva
0843447c41 Fix Portuguese translations for error messages (#16159)
* Fix Portuguese translations for error messages

Fix Portuguese translations for error messages

* Fix translation for RustDesk desktop session message

* Update ptbr.rs
2026-09-11 11:04:17 +08:00
bovirus
67e0a1f582 Update it.rs (#16157)
* Update it.rs

* Update it.rs

* Update it.rs
2026-09-11 10:28:51 +08:00
changshenhan
91c9fccbb0 chore(deps): security bumps in Cargo.lock (RUSTSEC-2026 fixes) (#16143)
Co-authored-by: changshenhan <217217832+changshenhan@users.noreply.github.com>
2026-09-10 17:06:24 +08:00
fufesou
c4221469d8 Fix/audio stream continuity (#16095)
* fix(audio): add streaming resampler

* fix(audio): preserve stream resampling state

* fix(audio): keep playback callback nonblocking

* fix(audio): decouple capture conversion from dasp

* fix(audio): support stateful samplerate backend

* refactor(audio): isolate stream callback state

* refactor(audio): group capture output options

* fix(audio): clear stale playback state after startup failure

Reset non-Linux playback state when stream startup fails to prevent
new-format audio from using the previous stream or resampler.

Add regression tests for failed format changes and successful playback.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): honor capture resampler selection and reuse buffers

Use the selected resampling backend for fixed-frame capture.
Convert samples directly into the input queue and
reuse the PCM frame buffer.

Add tests for anti-aliasing, thread transfer, and
partial-frame draining.

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact: reduce diffs

Signed-off-by: fufesou <linlong1266@gmail.com>

* test(audio): check resampler output count and passband energy

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): reset incompatible Linux playback state on
  startup failure

Preserve compatible output streams when replacement
  startup fails.
Clear state when no compatible stream exists and cover
  both paths in tests.

Signed-off-by: fufesou <linlong1266@gmail.com>

* perf(audio): reuse PCM buffers in the capture pipeline

- Reuse capture framing, resampling, and channel conversion buffers
- Deliver borrowed packets and write Sinc output into reusable storage
- Add allocation and output-equivalence regression tests

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): smooth buffer discard discontinuities

Signal receiver PCM discards and fade from the current playback output when the callback reaches the new timeline.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): add missing Cargo.toml

Signed-off-by: fufesou <linlong1266@gmail.com>

* perf(audio): move capture encoding off the CPAL callback

Move Opus encoding and service delivery to a dedicated worker.
Use a preallocated bounded PCM queue with explicit loss reporting.
Add tests for callback allocations and queue saturation.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): smooth capture gaps and report losses during backlog

Signed-off-by: fufesou <linlong1266@gmail.com>

* feat(audio): report capture queue high-water mark

Track peak queued PCM packets and log the approximate
queued audio duration alongside capture loss statistics.

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact(audio): reduce diffs

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): avoid blocking capture on encoder queue contention

Use preallocated queues with try_lock in the capture callback.
Count and drop the current packet on contention, preserving
drop-oldest behavior on overflow.

Add regressions for paused workers, buffer reuse, and sequence wrap.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: add the missing files

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): isolate zero-gate state per encoder

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact: reduce diffs

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact(audio): simple refactor

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): avoid waiting on playback callback locks

Use one PCM try_lock attempt and preserve queued samples during contention. Replace readiness locking with per-stream atomic status and report callback errors from the receiving thread.

Cover callback progress, retained audio, recovery, and poisoned-buffer handling.

* fix(audio): restart capture after processing errors

Stop further processing until the service recreates the stream.
Document the guard as defensive recovery for an unconfirmed failure.
Group capture and resampler submodules under their parent directories.

Signed-off-by: fufesou <linlong1266@gmail.com>

* audio: report capture queue contention drops separately

- Add contention_dropped to loss reports while preserving total drop counts
- Document packet rejection on contention even when buffers are available
- Extend existing contention and saturation test assertions

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact unit tests

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-10 16:00:58 +08:00
fufesou
978e2e28b9 fix(audio): restart capture when the device is unavailable (#16142)
* fix(audio): restart capture when the device is unavailable

* refact: remove low-value test

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-10 15:32:49 +08:00
rustdesk
5cfe136fb0 fix mac sign 2026-09-10 12:08:47 +08:00
Maison da Silva
14a5ed45d9 Revise full description for Android app pt-BR (#16140)
Updated documentation links and improved text clarity. pt-BR
2026-09-10 10:56:34 +08:00
Maison da Silva
435fe24a81 Fix formatting and punctuation in full_description.txt (#16138)
Fix formatting and punctuation in full_description.txt
2026-09-10 10:36:07 +08:00
Kauan Kelvin
3ffee7c1ff fastlane: add pt-BR Android metadata (#16135)
* fastlane: add pt-BR Android metadata

Signed-off-by: Kauan Kelvin <kelvinkauan722@gmail.com>

* Update fastlane/metadata/android/pt-BR/full_description.txt

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Signed-off-by: Kauan Kelvin <kelvinkauan722@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-09-10 10:33:54 +08:00
YannAntunes
97190f715b fastlane: add es-ES Android metadata (#16136)
Signed-off-by: Yann Antunes <yannantuneslopes123@gmail.com>
2026-09-10 09:58:31 +08:00
rustdesk
aa232a9dfa chore(flutter): pin our own git plugins instead of tracking their HEAD
dash_chat_2, window_manager and desktop_multi_window named only a url, so
the lock recorded `ref: HEAD` for them. That holds while the lock is used
verbatim, but any re-resolution -- an unrelated pubspec.yaml edit, a lock
conflict resolved by regenerating -- re-reads HEAD and silently moves the
plugin to whatever the fork's tip is that day. All three forks are ours and
get pushed to, which is how window_manager and desktop_multi_window drifted
across five lock bumps since July with no pubspec.yaml change to show for it.

Each ref is the resolved-ref already in the lock, and all three still match
their fork's HEAD, so nothing resolves differently today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-10 09:14:49 +08:00
fufesou
65edf214b9 fix(macos): recover system-stopped audio capture streams (#16123)
* fix(macos): recreate system-stopped audio capture streams

Pin CPAL's ScreenCaptureKit stop notifications and retain interruption
state with each capture stream. Recreate an interrupted stream through
the existing service restart path, outside the backend error callback,
and resend its audio format. Late callbacks cannot restart a replacement.

A natural -3821 stop was observed with the remote connection still open.
Its OS trigger remains unknown and it has no deterministic natural
reproducer. Controlled verification stops the real SCStream and delivers
an explicitly marked -3821 notification; this is not a natural failure.

Dependency: https://github.com/rustdesk-org/cpal/pull/5

Validation: requested macOS Rust and Flutter debug builds; three full-crate
regression tests; build check without ScreenCaptureKit; two controlled
recreations on one connection with independently recorded receiver audio.

* chore(macos): log audio capture startup and resumed samples

* Update deps, cpal

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-09 22:34:25 +08:00
RustDesk
bac8323e5d Wayland portal staged errors (#16118)
* 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, the request being dismissed, a timeout, the portal
being absent 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.

What the peer is told is chosen from the tag, and only from facts the tag
actually carries:

- A decline and an interaction that ended some other way are separate outcomes
  and say so. The Request spec defines response 1 as the user cancelling, and
  guarantees nothing more about 2 than that it ended -- libportal treats 2 as a
  plain failure -- so 2 says the request ended without completing and does not
  guess who ended it or why.
- A timeout says it timed out. It does not say nobody answered: RustDesk passes
  a saved `restore_token` with `persist_mode` 2, and a restored session is
  exactly the case where the portal shows no picker at all, so there may have
  been no dialog for anyone to answer.
- Not reaching the session bus, a portal that answers but does not implement
  what was called, and a grant that fails only when the PipeWire connection is
  handed over, each get their own message. None of the three is fixed by
  restarting the portal, so none of them is told to. Each says only what its
  evidence supports: failing to open the session bus does not prove nobody is
  logged in, and `UnknownMethod` on RemoteDesktop does not prove the portal
  cannot capture a screen. Which interface was called is in the D-Bus message
  that goes to the log; the message to the peer does not claim one.
- What is left -- the portal absent, silent, or failing mid-handshake -- keeps
  the existing `xdp-portal-unavailable`, which is already translated everywhere
  and carries the one remedy that fits: `systemctl --user restart
  xdg-desktop-portal`.
- The Ubuntu-before-21 branch keeps every outcome that says something about the
  machine and yields the three that say what happened to the request.

Two more say less than they could, for the same reason. `streams_from_response`
comes back empty when the response cannot be parsed as well as when there is
nothing in it, so the message says RustDesk did not obtain a usable screen
rather than that the portal offered none. `ElementFactory::make` fails the same
way for a plugin that is absent as for one that will not load, so the message
says the component could not be loaded rather than that it is missing.

The D-Bus error name and message, the portal response code and the GStreamer
factory's own error go to the log. Only the element name also reaches the peer,
because it is the one detail that says which package to look at.

`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. Every D-Bus
  call in the handshake -- opening the session bus, `get_request_path`, the
  `add_match` inside `handle_response`, `create_session`, and `conn.process` in
  the wait loop -- carries a tag, so a no-reply there is reported rather than
  fatal. 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

* wayland: lang keys for the staged portal failures

Eight 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.

One carries a `{}`, the name of the GStreamer element that could not be created
-- the one detail that tells a user which package to look at. `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. Everything
else technical stays in the log: a D-Bus error name or a portal response code
in a dialog is noise to the person reading it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 18:53:13 +08:00
RustDesk
f164c9a9df Dead peer recovery (#16117)
* webrtc: recover from a silent peer in about 8s instead of 30s

A controlled peer that is killed, switched away by a user switch, or rebooted
leaves no trace on a UDP transport: there is no reset to receive, so the session
sees silence, and only the 30s inactivity timeout ends it. By then the remote
machine may have finished rebooting and be reachable again, while the user has
been watching a frozen frame the whole time and is then told the peer reset the
connection.

ICE already knows sooner. It reports Disconnected about 5s after it stops
hearing from the peer, from its own task, so it stays accurate even while this
loop is busy sending. That state is transient by design - a Wi-Fi roam or a
sleep/wake recovers from it - so it is treated as suspicion, not as death: three
more seconds with the transport receiving nothing, and the session reconnects.
Receive progress cancels the suspicion, so a peer that is merely slow, or one
ICE was late to clear, is not dropped.

This only reaches the existing recovery sooner; it does not replace it. The
first reconnect goes out immediately and, if it fails, falls into the same
retry the UI already applies to any unexpected disconnect. The restart
reconnect event is reused deliberately: it is what asks for exactly that, with
no error dialog in front of it, and the UI shows "Connecting..." for it rather
than anything about restarting. Its five-minute grace stays reserved for a
restart the user actually asked for - silence is no evidence of a reboot.

The 30s timeout is unchanged and still backs every transport. TCP and WebSocket
are untouched. The controlled side is untouched: it detects a dead controller
on the same 30s, which wastes some capture but nothing a user sees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

* kcp: recover from a silent peer on the endpoint's own clock

KCP is the other transport with nothing to receive when the peer dies, and it
was the slower of the two: its endpoint reaps a connection only after 60s
without a packet, which is past the 30s inactivity timeout above it, so in
practice nothing but that timeout ever noticed.

The endpoint already tracks when each connection last heard from its peer and
now exposes it, so this reads that rather than anything derived from the session
loop - it keeps answering while that loop is busy sending. Its liveness ping now
goes out about every 2s rather than every 10s, so silence means the peer rather
than an idle link, and eight seconds of it is several missed pings.

Same threshold and the same recovery as the WebRTC half, so a user sees the same
thing on either transport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

* review: time the inactivity window off receive progress, bound the parting send

Two things the review found, both on the controlling side.

The 30s inactivity window still ran off completed messages alone, so the probe
added for the fast path did not fix what it was added for: a message larger than
the transport's fragment size yields nothing until its last fragment, and a peer
sending one steadily was still timed out mid-transfer. It is now timed off
whichever is later, a completed message or receive progress. Transports that
report no progress leave that at its starting value, so nothing else moves.

The parting close-reason send for KCP waited on send capacity with no deadline
of its own, and a queue a dead peer will never drain held the finished session's
thread until the endpoint reaped the connection a minute later. Bounded once the
peer has been declared gone. Still attempted rather than skipped: if the loss was
one-way the peer does receive it, and drops its side immediately instead of
waiting out its own timeout - which is also the one case where the note below
resolves itself.

Recorded from the same review, for the case none of this targets - a peer that
is alive behind a path that broke for five to ten seconds and then healed.
Giving up cannot deliver a close there, because the path is still down at that
moment, so the controlled side keeps the old connection until its own 30s
expires. For up to twenty of those seconds it holds two authorised connections:
its connection manager lists both, and the stale one reports a growing delay
that pins the shared frame rate low for the new one. Input is unaffected
throughout and both recover once the stale connection goes, so this trades
twenty-two seconds of a frozen, uncontrollable session for a controllable one
that looks wrong for a while. Closing the displaced connection is controlled-side
work and belongs with the rest of it, not here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

* review: reject a disconnected cached session, tidy the detector

hbb_common: `is_reusable_for` now also rejects a session ICE reports
Disconnected, so a caller is not handed one that already carries the hint; and
the receive-progress test no longer races `next()` against a sleeping sibling.

Here: the `is_some()` guard on the progress comparison was dead, since a
transport answers `None` for its whole life and `None != None` is already false.
The parting-send deadline is a `Duration` like every other constant around it
rather than bare milliseconds. And the comments are cut back to what is not
already evident from the code they sit on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

* review: keep the legacy UI's retrying error when the peer goes silent

`restarting-show` is a Flutter control event; Sciter has no case for it and
falls through to a plain dialog, which `check_if_retry` marks non-retryable
because its type is not `error`. So on that build the new detector would have
replaced a timeout that reconnects on its own after 30s with a dialog waiting
for a click at 8s - a regression for the one path this was meant to shorten.

Send it the message the timeout already sends, so its behaviour is unchanged
apart from arriving sooner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

* review: keep the 30s watchdog hard, and let Android's picker hold the reconnect

Timing the watchdog off receive progress gave away its upper bound. A fragment
bumps the counter as it arrives, ahead of the framing checks that would reject
it, so a peer sending one `FRAG_MORE` every twenty seconds and never a
`FRAG_END` refreshed the deadline forever while the reassembly buffer grew
toward `MAX_FRAME_LENGTH`, a gigabyte away. What it bought - a clipboard image
that takes longer than thirty seconds to arrive is not a dead peer - is a
pre-existing problem that predates this branch and can be fixed on its own.
Receive progress goes back to the one job it was added for, which needs no
deadline of its own: telling a transport that has gone quiet from one that is
still delivering, so ICE's disconnected hint is not acted on mid-transfer.

The Android document picker suppresses a `Connection Error` while it is open
and remembers to reconnect once it closes. The peer-gone break reconnects
under `restarting-show` with a `Connecting...` title, which matched neither
half of that test, so an eight-second stall behind an open picker - Doze and
background throttling produce them - threw a dialog up behind the picker and
lost the deferred reconnect. It is now named there by its own title rather
than by its type: an explicitly restarted remote device sends the same type
from a path this leaves alone, on every transport, and deferring that one too
would be a change to sessions this has no business touching.

The two limits are still not hard upper bounds, and the comment saying so was
wrong about why. A send is awaited inline in this loop, so one in progress
delays the tick that checks them - bounded on WebRTC by the timeout the stream
was built with, not bounded at all on KCP, whose framed stream is constructed
with none. The 30s watchdog beside it shares the loop and the same delay.

Left alone deliberately. `restarting-show` reconnects without the backoff its
`restarting` sibling uses, which can loop while each round gets far enough to
establish a session and then loses the transport within eight seconds; a
cooldown there would also delay the recovery this exists for when a peer
really does come back, and the loading it shows can be cancelled. And the KCP
limit reads an accumulated silence rather than a transient hint, so unlike the
WebRTC grace it needs no second sample to confirm - one would only move eight
seconds to nine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 17:56:41 +08:00
21pages
080211ff36 improve qos (#16082)
* first improve

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(qos): avoid reducing FPS for transient network jitter

  Require consecutive bad samples to confirm congestion before normal
  FPS reductions. Prefer bitrate reduction when ABR is available and
  allow an outstanding probe to complete before evaluating its effect.

  Recover FPS faster on fresh good samples while preserving severe-delay
  and timeout protection.

  Add regression coverage for jitter, bandwidth changes, and multiple viewers.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* test(qos): virtual clock and a closed-loop link simulation

Tests drove time by moving `Instant`s into the past, which panics on a host
that booted less than two minutes ago, and the ABR smoke had to detect ratio
changes to keep its fake clock consistent.  `VideoQoS` now reads the clock
through `now()`; tests set a virtual instant and advance it.

`tests/sim.rs` drives the controller the way `Connection` does, over a link
with variable frame sizes, wobbling capacity, heavy-tailed jitter,
retransmission stalls and link stalls, with both a bitrate-targeted
(VP8/VP9/AV1) and a fixed-rate (hardware) encoder model.  It prints one
table row per scenario; the assertions arrive with the controller changes.
The short-stall smoke sweeps the stall phase instead of three fixed values.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* fix(qos): keep jittery but healthy links fast, drain congestion with bitrate

A weak home Wi-Fi with plenty of capacity but frequent jitter and the odd
stall ended up at about 10 fps: single bad replies, the two second probe
timeout and a slow climb back each took their share.  The controller now
treats a transient stall and a saturated link differently.

- The probe timeout no longer slams the frame rate to 2.  Every second the
  probe stays out beyond the first halves it instead, and the late reply
  that finally closes the probe does not reduce again.
- After a reduction, good replies return halfway, then fully, to the level
  held before it.  A restored level that congests within five replies
  becomes a lower ceiling, so a real capacity drop converges instead of
  oscillating.
- VP8, VP9 and AV1 run CBR against timestamps: fewer frames only means
  bigger frames.  While the bitrate can still be reduced the frame rate
  keeps its floor, and three bad replies in a row confirm congestion and
  halve the bitrate instead of stepping it down by a fifth every three
  seconds.
- `bitrate_first` now means the bitrate can actually still drop; at the
  floor, congestion during the adjustment cooldown reduces the frame rate
  (Greptile).
- `avg_delay()` subtracts the baseline with `saturating_sub`; at exactly
  the baseline it returned the whole delay, which kept the bitrate of a
  stable high-RTT link from ever recovering.
- `HISTORY_DELAY_LEN` kept three samples, not two.
- Every ratio adjustment resets the dynamic screen counters, so a long
  congestion episode cannot make a static screen look dynamic
  (CodeRabbit).
- One `qos_trace` debug line per probe reply and per timeout, for replay
  through `tests/sim.rs`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* fix(qos): adapt each viewer from its own target, not the shared minimum

`user_network_delay` started every viewer's adaptation from `self.fps`,
the minimum over all viewers.  One congested viewer therefore pulled the
others' targets down with it, and when it recovered the stream stayed low
until the others had climbed back on their own.  The per-viewer memory
added for restores made the coupling worse: a viewer recorded another
viewer's low rate as its own pre-congestion level (Greptile).

Each viewer now adapts from its own `delay.fps`, falling back to the shared
value only for its first reply; `adjust_fps` keeps aggregating the minimum.
The replay test now fails on a malformed trace value instead of dropping
it (CodeRabbit).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* test(qos): paired network trace, bitrate-conserving encoder, twenty seeds

The simulator drew every random quantity from one stream, so two controllers
with the same seed saw different links as soon as they produced a different
number of frames; the A/B was not paired.  The link trace (capacity wobble,
stalls, loss events) is now generated before the run from a network stream of
its own, and encoder noise and probe jitter have separate streams.

The CBR model let a scene change add three frames' worth of data every five
seconds without clawing it back, which raised the offered load of any
controller that lowered the frame rate (up to +27% at 2 fps).  The encoder now
repays the surplus over the following frames and the size spread has mean one,
so the long-term load no longer depends on the frame rate.

Every scenario runs over twenty seeds and the assertions bound the
distribution: median of the mean target, worst p10, p90 of the time below half
the limit and of the queue p95.  The bounds state what the product needs, not
what one seed produced.  New columns: produced and delivered frames per
second, delivered frame age, sustained recovery (target at the limit and queue
under 200 ms held for five seconds), cold-start minimum and time to 90% of the
limit.  The replay advances by recorded `t=` deltas when present and is
labelled as the open-loop, FPS-only diagnostic it is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* fix(qos): baseline from the first reply, bitrate cuts on confirmation only

Two findings from the design re-check.

The baseline needed ten replies before it was used, so a stable 180 or
300 ms link spent its first ten seconds read as congested: the frame rate
fell to 5 and the bitrate was cut before either recovered.  The running
minimum is the baseline from the first reply on; the smoothed estimate
takes over once the window is full.

A single reply a second above the baseline, or a single timer tick with the
probe out for two seconds, cut the bitrate by a fifth.  A static screen never
earns an increase back, so repeated stalls ratcheted its ratio down and the
next dynamic episode started from there.  Bitrate cuts now need two bad
replies in a row, or a probe still outstanding at the second tick past two
seconds; the frame rate brake and the severe-reply rule are unchanged.  An
elevated but unconfirmed delay no longer restarts the ratio cooldown, so a
confirmation on the next reply is acted on at once.

`qos_trace` lines carry a millisecond timestamp for the replay test.  The
simulator asserts the intercontinental cold start: minimum target at
INIT_FPS and 90% of the limit within ten seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* test(qos): held-out seeds and parameter sensitivity as guards against tuning

The scenario bounds now live in one function shared by the CI run over seeds
1 to 20 and by `robustness.rs`, whose two ignored tests apply the same bounds
to seeds 21 to 120 in blocks of twenty and halve or double each scenario
parameter in turn.  Anyone changing a controller constant or a bound runs
them; a bound that fails on unseen seeds was fitted to the CI seeds.

At this head every held-out block passes, with medians within a few percent
of the CI seeds, while master fails five of five blocks in every home Wi-Fi
row.  The sweep keeps the lead over master in all fourteen variants for the
frame-rate metrics and shows two limits worth knowing: at 6 Mbps of capacity
both controllers hold about 2.4 s of queue p95, and at a drop to 1.5 Mbps
both are poor because the 1 Mbps bitrate floor leaves little to drain with.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* test(qos): frame age bounds, wall-clock scene changes, time-indexed probe jitter

The queue metric divides the queued bytes by the nominal capacity, so during a
link stall it reports how long the backlog takes to drain afterwards, not how
long the frame at the head has already waited.  Frame age, the time a
delivered frame spent in the shared path, was computed but not bounded.  It
is now bounded per scenario, as a regression bound set from the scenario
rather than from a run: 1.5 s on the home Wi-Fi rows (isolated stalls of up
to 2.5 s are tolerated, a sustained multi-second backlog is not), 100 ms on a
clean link, 150 ms on a stable high-RTT link (frame age excludes the round
trip, so RTT earns no allowance), and the same bound as the queue on the
bandwidth-drop and mobile rows.

Two residual couplings between controller decisions and the exogenous inputs
are removed: scene changes follow the wall clock instead of the frame count,
and probe jitter is a per-second table drawn before the run, so two
controllers with the same seed meet the same content timeline and the same
jitter.  The moderate-congestion smoke profile now asserts that the frame
rate actually drops, and the held-out test builds its combined summary from
the block reports instead of simulating every seed twice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* test(qos): remove the placeholder viewer from trace replay

  Clear the synthetic viewer created by smoke::session so it cannot
  cap replayed FPS at 15 when the recorded connection ID differs from 1.

  Add a regression test verifying identical FPS sequences for connection
  IDs 1 and 1652, both recovering to 30 FPS.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(qos): cap each viewer by its own limit, judge bitrate steps per viewer

Two places still let one viewer's state leak into another's.

The per-viewer target was clamped by `highest_fps()`, the minimum of every
viewer's limit, before being stored, and a new viewer started from the shared
stream rate.  A viewer that lowered its limit dragged the others' targets down
with it, and when it left the stream stayed there until the others had climbed
back; a viewer joining a congested session started at the congested rate.
Each viewer now starts at INIT_FPS, is capped by its own limit only, and
`adjust_fps` keeps applying the shared limit to the aggregate.

`adjust_ratio` paired the maximum delay over viewers with any viewer's
confirmation, so one viewer's unconfirmed 1200 ms spike and another viewer's
two 200 ms replies produced a 20% cut, and a third mild reply a halving, when
each viewer on its own called for five percent.  Each viewer's own delay and
confirmation now decide the step it calls for, and the stream takes the most
conservative one; increases still need every viewer below the threshold.
Single-viewer behaviour is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* test(qos): replay advances by the wall clock across connections

The replay kept a last timestamp per connection id, so a log with several
viewers each writing once a second advanced the virtual clock once per line
and replayed several times slower than it was recorded.  It now advances by
the delta between consecutive lines whatever their connection.  The replay is
a plain function over the log text, with the environment-driven test as its
entry point, so the time axis can be tested directly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* fix(qos): first reply keeps the ratio cooldown, closing a viewer re-aggregates

A viewer's first TestDelay reply called `adjust_ratio` with no cooldown
check.  With the per-viewer steps, that scan finds the other viewers' still
confirmed evidence, so a viewer joining right after a cut halved the bitrate a
second time inside the three seconds the cut is meant to be observed for.  The
first reply is now one more trigger of the periodic adjustment and keeps its
cooldown; a fresh session still adjusts on its first reply, since its
controller was created long before.  Linux was never on this path.

`on_connection_close` removed the viewer without re-aggregating, so the
stream stayed at the departed viewer's rate until the next tick; the
remaining viewers are aggregated at once.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* fix(qos): the newcomer guard belongs to the viewer that joined

Re-aggregating on close applied the one-second INIT_FPS guard that the
departing viewer had set when it joined, so a viewer that connected and
dropped within a second throttled the others to 15 fps for the rest of that
second.  The guard now records which viewer set it and is cleared when that
viewer leaves; a genuinely new viewer is still capped for its first second.

The first-reply ratio adjustment's platform switch is a field instead of a
`cfg!` inside the condition, so the cooldown regression test exercises the
path on Linux CI as well.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* fix(qos): every newcomer carries its own start-up guard

The start-up guard had one slot, so a second viewer joining within a second
overwrote the first one's join time, and when the second viewer left the
first one's window was released early.  The join time now lives in the
viewer's own entry, `adjust_fps` caps the stream while any viewer is inside
its first second, and a departed viewer takes its guard with its entry; no
clearing logic is needed (Greptile).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* fix(qos): adapt delay baselines and speed up FPS recovery

  - Relearn stable baseline increases from recent fresh replies.
  - Require fresh congestion confirmation between ordinary FPS reductions.
  - Keep automatic FPS reductions above a 5 FPS floor, respecting lower caps.
  - Restore FPS after two good replies, with rollback on renewed congestion.
  - Add regression tests for baseline changes, jitter, and FPS recovery.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* test(qos): align FPS floor tests with the 5 FPS minimum

  - Remove the unregistered sustained tests with outdated expectations.
  - Test severe delays and timeouts independently, including lower user
    caps and recovery.
  - Move the 700 kbps scenario into active adaptation tests, checking
    the FPS floor and recovery after bandwidth returns.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(qos): a timeout never lifts a target, and the invariants as property tests

The timeout brake floored its output at MIN_FPS + 1, so a viewer whose target
had already reached 1 fps was lifted to 2 by the next tick past two seconds:
bad evidence raising the frame rate.  Inherited from master, where the timeout
set the whole stream to MIN_FPS + 1 outright.  The brake now never exceeds the
target it found, whatever the elapsed time it is told.

The controller's six invariants become property tests over random sessions
(150 seeds, 300 steps, one to three viewers, ABR on and off): a viewer's
target is independent of other viewers; bad evidence never raises a target or
the ratio; joins and leaves only change the aggregation; a bitrate cut is
owned by a viewer's own evidence and not spent again by a newcomer; targets
stay within their caps and the stream is their aggregation; a braked probe's
late reply does not brake again.  The timeout case is also pinned exhaustively
over every target and a range of elapsed times.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* feat(qos): log the encode and send pipeline behind RUSTDESK_QOS_VERBOSE

The controller's target frame rate is neither the rate the encoder produced
nor the rate the send path accepted, and two facts kept that gap invisible.
libvpx drops frames on its own when the bitrate cannot carry them, so a
capture round is not a delivered frame.  The video send is inline in the
connection's message loop, so a slow write stalls capture and the delay
probe alike, and the recorded delay cannot tell the two apart.

`qos_video` reports, per second and per display, the capture rounds, the
frames that actually reached a connection, and the longest wait for the
previous frame to be picked up.  `qos_send` reports, per second and per
connection, how long `stream.send().await` blocked and how deep the video
queue is.  Both carry `t=`, so they join with `qos_trace` offline; `replay`
filters on `qos_trace` and ignores them.

The default log level is `debug`, so an unconditional line would land in
every user's log file once a second forever.  Neither line is emitted
unless `RUSTDESK_QOS_VERBOSE` is set, nothing enables it implicitly, and
without it the timing calls are skipped as well.  TestDelay, the controller
and every threshold are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* fix(qos): speed up FPS ramp-up on clean connections

  Double startup FPS after every two fresh low-excess-delay replies,
  up to the viewer's cap. End acceleration on queue growth, timeout,
  or FPS reduction.

  Add regression tests for startup speed, viewer caps, congestion,
  timeouts, and multiple viewers.

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:48:32 +08:00
RustDesk
01dbb76499 server: do not lock the screen for a connection a reconnect replaced (#16124)
A controlling peer whose link dies without a close reconnects, while the
connection it left behind runs on here until its own 30s inactivity timeout.
That one then ends with `on_close("Timeout", true)`, and the lock is gated
only on `lock_after_session_end` and this connection's own `keyboard` - both
set by the very controller that is at that moment working in the session its
reconnect re-established. Nothing anywhere asks whether the session is still
being controlled, so the screen locks under a peer that came back twenty-odd
seconds earlier, and the operator's desk locks itself in front of them.

The lock now also requires that no newer remote control connection of this
session is authorized.

Newer, not merely other. A connection stays in `AUTHED_CONNS` until its
`AuthedConnID` drops, which is well after `on_close` returns, so a symmetric
test would have two of one session ending together each see the other and
neither lock. Ids come from a counter, so `>` orders them: of a session's
connections the last still locks, whether they end one after another or at
once.

Remote control only, and this session only. The other kinds do not keep a
screen in use, and `send_logon_response` clears `keyboard` for a file
transfer, a terminal and a camera view, so none of those reaches the gate at
all - a port forward keeps it, and is kept out only by the client not sending
`lock_after_session_end` on one. Another peer's
session is left exactly as it is: whether its ending locks the screen while
this one is connected is a separate question, and not one a timeout on this
side should start answering.

Every close that asked to lock, not only the timeout. A connection its own
peer has already replaced should not lock the session that replaced it
however it ends, and singling out one reason would leave the same race
reachable through the others.

Two things it does not cover. A reconnect that has not authorized yet is not
in `AUTHED_CONNS`, so a timeout landing while one waits at the accept prompt
or on 2FA locks as before. And the lock is skipped, not handed on: if the
connection that replaced this one later ends through a path that asks not to
lock - a failed send, a stopped service - nothing locks. That is what those
paths already choose for a connection dying of a network error, on the
assumption that the peer will retry, and it is the same assumption this makes.


Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 15:06:24 +08:00
Sid
68359a2dd2 fix(macOS): preserve release entitlements when signing (#16125)
* fix(macOS): preserve release entitlements during signing

Signed-off-by: Sidn <3996515+sidnvy@users.noreply.github.com>

* ci(macOS): sign outer app with release entitlements

Signed-off-by: Sidn <3996515+sidnvy@users.noreply.github.com>

---------

Signed-off-by: Sidn <3996515+sidnvy@users.noreply.github.com>
2026-09-09 14:48:59 +08:00
cui fliter
5228f91982 fix(screenshot): keep cached image when saving fails (#16120)
Signed-off-by: cuishuang <imcusg@gmail.com>
2026-09-09 07:46:19 +08:00
RustDesk
691830fe89 bump webrtc: revert the T3-rtx probe recovery (#16121)
The probe recovery merged as 692113c87 cost two to four times the p99 on the
workload a remote desktop actually has, and the fork now reverts it: `sctp/src`
returns to 48100bf1, the revision this repository shipped in #15684, with the
benchmark harness and its corrections kept.

It was justified on a fixed frame rate. Nothing is sent while the screen holds
still - the capturer answers WouldBlock and the loop sends nothing - so typing,
reading and clicking are short bursts with silence between them, and a steady
frame rate is what playing video or dragging a window looks like and nothing
else. The difference matters because a steady rate hides the whole effect: the
next frame's SACK exposes a loss whatever the recovery logic does. Measured on
bursts with gaps, after correcting two faults in the harness itself, p99 in ms
for the two seeds:

                          sparse RTT70   sparse RTT150
    48100bf1  09-06 00:06   200 / 208      407 / 507
    b221f13b  09-06 14:00   328 / 804      737 / 826

On a fixed frame rate the two are within noise of each other, which is why this
was not caught. KCP is 138/130 and 257/250 on those rows, ahead of both.

What is given up: a tail loss of exactly four packets recovers in 140 ms rather
than 229, and an idle sender's backlog after a stall offers 1.58x the bytes
rather than 1.08x. A five-packet tail improves, 292 ms to 232.


Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:31:05 +08:00
fufesou
22b1ed169a fix(audio): update CPAL for WASAPI thread priority (#16110)
* fix(audio): update CPAL for WASAPI thread priority

* update cpal

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-08 16:19:16 +08:00
Jamal Ali
0f0205d336 feat: add Azerbaijani translation (#16103)
Signed-off-by: Jamal <jamalkamaladdin@gmail.com>
2026-09-08 15:20:27 +08:00
126 changed files with 10149 additions and 827 deletions

42
.github/scripts/sign-macos-app.sh vendored Normal file
View File

@@ -0,0 +1,42 @@
#!/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,6 +374,12 @@ jobs:
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
with:
# Select the MSBuild process architecture; -p:Platform sets the MSI target.
# Native ARM64 tools give the compiler more address space for PCH files
# (C3859/C1076 were reported by HostX86\arm64\CL.exe).
# Keep the action's default x86 MSBuild for the existing x64 MSI job.
msbuild-architecture: ${{ matrix.job.arch == 'aarch64' && 'arm64' || 'x86' }}
- name: Build msi
# Builds the MSI for the matrix arch. res/msi (WiX v4 + native CustomActions) carries
@@ -492,11 +498,20 @@ jobs:
version: ${{ env.LLVM_VERSION }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: nightly-2023-10-13-${{ matrix.job.target }} # must use nightly here, because of abi_thiscall feature required
targets: ${{ matrix.job.target }}
components: "rustfmt"
shell: bash
run: |
# Sciter's abi_thiscall feature requires nightly Rust.
# Use an i686 host toolchain so build scripts can load the 32-bit LLVM installed above.
# Since rustup 1.29.1, i686 toolchains on x64 Windows require --force-non-host
# for both installation and default selection, even though WOW64 can run them.
# See https://github.com/rust-lang/rustup/pull/4935.
# Alternatively, an x64 compiler could use cargo build --target i686-pc-windows-msvc.
# This requires 64-bit LLVM for host build scripts and updated packaging paths,
# and has not been manually verified for RustDesk.
rustup toolchain install nightly-2023-10-13-${{ matrix.job.target }} \
--target ${{ matrix.job.target }} --component rustfmt \
--profile minimal --no-self-update --force-non-host
rustup default nightly-2023-10-13-${{ matrix.job.target }} --force-non-host
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
@@ -926,7 +941,11 @@ jobs:
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg
rm -rf *.dmg || true
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
# the identity secret carries its own shell quoting, so expand it inline like the dmg codesign below
bash ./.github/scripts/sign-macos-app.sh \
./flutter/build/macos/Build/Products/Release/RustDesk.app \
${{ secrets.MACOS_CODESIGN_IDENTITY }} \
./flutter/macos/Runner/Release.entitlements
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg

60
Cargo.lock generated
View File

@@ -985,9 +985,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
dependencies = [
"serde 1.0.228",
]
@@ -1717,7 +1717,7 @@ dependencies = [
[[package]]
name = "cpal"
version = "0.15.3"
source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#6b374bcaed076750ca8fce6da518ab39b882e14a"
source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#96d4da121b7d949677ac5b6887413a9185fd7f39"
dependencies = [
"alsa",
"cidre",
@@ -1791,9 +1791,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
@@ -4121,8 +4121,7 @@ dependencies = [
[[package]]
name = "interceptor"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac0781c825d602095113772e389ef0607afcb869ae0e68a590d8e0799cdcef8"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"async-trait",
"bytes",
@@ -4280,7 +4279,7 @@ dependencies = [
[[package]]
name = "kcp-sys"
version = "0.1.0"
source = "git+https://github.com/rustdesk-org/kcp-sys?branch=rustdesk-patches#023a0065398968989f2ddfcf5cc72bb886d02675"
source = "git+https://github.com/rustdesk-org/kcp-sys?branch=rustdesk-patches#938eda3e5e9757a612385503af7a6cb1189b2cdd"
dependencies = [
"anyhow",
"auto_impl",
@@ -6496,9 +6495,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.13"
version = "0.11.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
dependencies = [
"bytes",
"getrandom 0.3.2",
@@ -7008,8 +7007,7 @@ dependencies = [
[[package]]
name = "rtcp"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9689528bf3a9eb311fd938d05516dd546412f9ce4fffc8acfc1db27cc3dbf72"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"bytes",
"thiserror 1.0.61",
@@ -7019,8 +7017,7 @@ dependencies = [
[[package]]
name = "rtp"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c54733451a67d76caf9caa07a7a2cec6871ea9dda92a7847f98063d459200f4b"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"bytes",
"memchr",
@@ -7139,6 +7136,7 @@ dependencies = [
"lazy_static",
"libpulse-binding",
"libpulse-simple-binding",
"libsamplerate-sys",
"libxdo-sys",
"mac_address",
"magnum-opus",
@@ -7464,8 +7462,7 @@ dependencies = [
[[package]]
name = "sdp"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd277015eada44a0bb810a4b84d3bf6e810573fa62fb442f457edf6a1087a69"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"rand 0.8.5",
"substring",
@@ -8037,8 +8034,7 @@ dependencies = [
[[package]]
name = "stun"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dbc2bab375524093c143dc362a03fb6a1fb79e938391cdb21665688f88a088a"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"base64 0.22.1",
"crc",
@@ -8920,8 +8916,7 @@ dependencies = [
[[package]]
name = "turn"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f5aea1116456e1da71c45586b87c72e3b43164fbf435eb93ff6aa475416a9a4"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -9584,8 +9579,7 @@ dependencies = [
[[package]]
name = "webrtc"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24bab7195998d605c862772f90a452ba655b90a2f463c850ac032038890e367a"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"arc-swap",
"async-trait",
@@ -9628,8 +9622,7 @@ dependencies = [
[[package]]
name = "webrtc-data"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e97b932854da633a767eff0cc805425a2222fc6481e96f463e57b015d949d1d"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"bytes",
"log",
@@ -9643,8 +9636,7 @@ dependencies = [
[[package]]
name = "webrtc-dtls"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ccbe4d9049390ab52695c3646c1395c877e16c15fb05d3bda8eee0c7351711c"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"aes",
"aes-gcm",
@@ -9680,8 +9672,7 @@ dependencies = [
[[package]]
name = "webrtc-ice"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb51bde0d790f109a15bfe4d04f1b56fb51d567da231643cb3f21bb74d678997"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"arc-swap",
"async-trait",
@@ -9705,8 +9696,7 @@ dependencies = [
[[package]]
name = "webrtc-mdns"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "979cc85259c53b7b620803509d10d35e2546fa505d228850cbe3f08765ea6ea8"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"log",
"socket2 0.5.10",
@@ -9718,8 +9708,7 @@ dependencies = [
[[package]]
name = "webrtc-media"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80041211deccda758a3e19aa93d6b10bc1d37c9183b519054b40a83691d13810"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"byteorder",
"bytes",
@@ -9731,7 +9720,7 @@ dependencies = [
[[package]]
name = "webrtc-sctp"
version = "0.12.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=b221f13b1d6f21fbce09f9f63096be27dd392265#b221f13b1d6f21fbce09f9f63096be27dd392265"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"arc-swap",
"async-trait",
@@ -9748,8 +9737,7 @@ dependencies = [
[[package]]
name = "webrtc-srtp"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01e773f79b09b057ffbda6b03fe7b43403b012a240cf8d05d630674c3723b5bb"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"aead",
"aes",
@@ -9771,7 +9759,7 @@ dependencies = [
[[package]]
name = "webrtc-util"
version = "0.11.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=b221f13b1d6f21fbce09f9f63096be27dd392265#b221f13b1d6f21fbce09f9f63096be27dd392265"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
dependencies = [
"async-trait",
"bitflags 1.3.2",

View File

@@ -22,7 +22,7 @@ path = "src/service.rs"
[features]
inline = []
use_samplerate = ["samplerate"]
use_samplerate = ["samplerate", "libsamplerate-sys"]
use_rubato = ["rubato"]
use_dasp = ["dasp"]
flutter = ["flutter_rust_bridge"]
@@ -67,6 +67,7 @@ magnum-opus = { git = "https://github.com/rustdesk-org/magnum-opus" }
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
rubato = { version = "0.12", optional = true }
samplerate = { version = "0.2", optional = true }
libsamplerate-sys = { version = "0.1.12", optional = true }
uuid = { version = "1.3", features = ["v4"] }
num_cpus = "1.15"
bytes = { version = "1.4", features = ["serde"] }
@@ -230,8 +231,11 @@ libxdo-sys = { path = "libs/libxdo-sys-stub" }
# the SACK settle the rest (F-RTO), timed from the latest send, so a stall no longer resends the
# whole backlog behind itself while a short lost tail still comes back at once.
# Pinned by rev, not branch: a fork branch can be rewritten out from under the lockfile.
webrtc-util = { git = "https://github.com/rustdesk-org/webrtc", rev = "b221f13b1d6f21fbce09f9f63096be27dd392265" }
webrtc-sctp = { git = "https://github.com/rustdesk-org/webrtc", rev = "b221f13b1d6f21fbce09f9f63096be27dd392265" }
# webrtc: SettingEngine cannot reach the ICE agent's max_binding_requests, which decides how
# long the answerer keeps checking a pair that has not answered yet.
webrtc = { git = "https://github.com/rustdesk-org/webrtc", rev = "80d5a20532cf58f5d4d237c437a98ceb85ee40dc" }
webrtc-util = { git = "https://github.com/rustdesk-org/webrtc", rev = "80d5a20532cf58f5d4d237c437a98ceb85ee40dc" }
webrtc-sctp = { git = "https://github.com/rustdesk-org/webrtc", rev = "80d5a20532cf58f5d4d237c437a98ceb85ee40dc" }
[package.metadata.winres]
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."

View File

@@ -1,4 +1,4 @@
FROM debian:bullseye-slim
FROM debian:trixie-slim
WORKDIR /
ARG DEBIAN_FRONTEND=noninteractive

View File

@@ -0,0 +1,11 @@
Aplicación de escritorio remoto de código abierto, la alternativa open source a TeamViewer.
Código fuente: https://github.com/rustdesk/rustdesk
Documentación: https://rustdesk.com/docs/en/manual/mobile/
Para que un dispositivo remoto controle tu Android mediante el ratón o el tacto, debes permitir que RustDesk utilice el servicio de "Accesibilidad". RustDesk utiliza la API AccessibilityService para implementar el control remoto en Android.
Además del control remoto, también puedes transferir archivos fácilmente entre dispositivos Android y ordenadores mediante RustDesk.
Tienes control total de tus datos, sin preocupaciones de seguridad. Puedes utilizar nuestro servidor rendezvous/relay, optar por el autoalojamiento o escribir tu propio servidor rendezvous/relay. El servidor autoalojado es gratuito y de código abierto: https://github.com/rustdesk/rustdesk-server
Descarga e instala la versión de escritorio desde: https://rustdesk.com — entonces podrás acceder y controlar tu ordenador desde tu teléfono, o controlar tu teléfono desde tu ordenador.

View File

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

View File

@@ -0,0 +1,11 @@
Applicazione open source per desktop remoto, l'alternativa open source a TeamViewer.
Codice sorgente: https://github.com/rustdesk/rustdesk
Documentazione: https://rustdesk.com/docs/it/client/android/
Per consentire a un dispositivo remoto di controllare il tuo Android tramite mouse o tocco, devi permettere a RustDesk di utilizzare il servizio "Accessibilità". RustDesk utilizza l'API AccessibilityService per implementare il controllo remoto su Android.
Oltre al controllo remoto, puoi anche trasferire facilmente file tra dispositivi Android e PC con RustDesk.
Hai il controllo totale dei tuoi dati, senza preoccupazioni per la sicurezza. Puoi usare il nostro server rendezvous/relay, optare per l'hosting autonomo, o scrivere il tuo server rendezvous/relay. Il server auto-ospitato è gratuito e open source: https://github.com/rustdesk/rustdesk-server
Scarica e installa la versione per desktop da: https://rustdesk.com — potrai quindi accedere e controllare il tuo computer dal cellulare, o controllare il tuo cellulare dal computer.

View File

@@ -0,0 +1 @@
App open source per desktop remoto, alternativa a TeamViewer.

View File

@@ -0,0 +1,11 @@
Aplicativo de desktop remoto de código aberto, a alternativa open source ao TeamViewer.
Código-fonte: https://github.com/rustdesk/rustdesk
Documentação: https://rustdesk.com/docs/pt/client/android/
Para que um dispositivo remoto controle seu Android via mouse ou toque, você precisa permitir que o RustDesk utilize o serviço de "Acessibilidade". O RustDesk usa a API AccessibilityService para implementar o controle remoto no Android.
Além do controle remoto, você também pode transferir arquivos entre dispositivos Android e PCs com facilidade usando o RustDesk.
Você tem controle total dos seus dados, sem preocupações com a segurança. Você pode usar nosso servidor rendezvous/relay, optar pela auto-hospedagem ou criar seu próprio servidor de rendezvous/relay. O servidor auto-hospedado é gratuito e open source: https://github.com/rustdesk/rustdesk-server
Baixe e instale a versão para desktop em: https://rustdesk.com — então você poderá acessar e controlar seu computador pelo celular ou controlar seu celular pelo computador.

View File

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

View File

@@ -51,6 +51,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
private var minBufferSize = 0
private var audioRecordStat = false
private var audioThread: Thread? = null
private var playbackCapturePending = false
@RequiresApi(Build.VERSION_CODES.M)
fun createAudioRecorder(inVoiceCall: Boolean, mediaProjection: MediaProjection?): Boolean {
@@ -193,6 +194,17 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
return audioRecorder?.audioSource == MediaRecorder.AudioSource.VOICE_COMMUNICATION
}
fun getVoiceCallStartError(): String {
return if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_GRANTED) {
"To start a voice call, enable \"Audio capture\" on the \"Screen share\" page."
} else {
"Failed to start voice call."
}
}
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
if (!isSupportVoiceCall()) {
return false
@@ -220,6 +232,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
if (it.getAudioSource() == MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
return true
}
playbackCapturePending = true
}
audioRecordStat = false
audioThread?.join()
@@ -234,11 +247,10 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
@RequiresApi(Build.VERSION_CODES.M)
fun switchOutVoiceCall(mediaProjection: MediaProjection?): Boolean {
audioRecorder?.let {
if (it.getAudioSource() != MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
return true
}
if (!isVoiceCallActive() && !playbackCapturePending) {
return true
}
playbackCapturePending = true
audioRecordStat = false
audioThread?.join()
@@ -246,7 +258,11 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
Log.e(logTag, "createAudioRecorder fail")
return false
}
return startAudioRecorder()
val started = startAudioRecorder()
if (started) {
playbackCapturePending = false
}
return started
}
fun tryReleaseAudio() {
@@ -256,6 +272,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
audioRecordStat = false
audioThread?.join()
audioThread = null
playbackCapturePending = false
}
fun destroy() {

View File

@@ -969,7 +969,7 @@ class MainActivity : FlutterActivity() {
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to "Failed to start voice call."))
"text" to audioRecordHandle.getVoiceCallStartError()))
} else {
Log.d(logTag, "onVoiceCallStarted success")
}

View File

@@ -153,19 +153,13 @@ class MainService : Service() {
} else {
if (!switchOutVoiceCall()) {
Log.e(logTag, "switchOutVoiceCall fail")
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to "Failed to switch out voice call."))
showVoiceCallError("Failed to switch out voice call.")
}
}
} else {
if (!switchToVoiceCall()) {
Log.e(logTag, "switchToVoiceCall fail")
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to "Failed to switch to voice call."))
showVoiceCallError(audioRecordHandle.getVoiceCallStartError())
}
}
} catch (e: JSONException) {
@@ -507,6 +501,15 @@ class MainService : Service() {
}
}
private fun showVoiceCallError(message: String) {
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to message))
}
}
@Synchronized
private fun startMicrophoneCapture(startAudio: () -> Boolean): Boolean {
if (!setMicrophoneForegroundService(true)) {

View File

@@ -2,6 +2,7 @@ package com.carriez.flutter_hbb
import android.app.Activity
import android.content.Intent
import android.media.projection.MediaProjectionConfig
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Bundle
@@ -19,7 +20,13 @@ class PermissionRequestTransparentActivity: Activity() {
ACT_REQUEST_MEDIA_PROJECTION -> {
val mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val intent = mediaProjectionManager.createScreenCaptureIntent()
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
mediaProjectionManager.createScreenCaptureIntent(
MediaProjectionConfig.createConfigForDefaultDisplay()
)
} else {
mediaProjectionManager.createScreenCaptureIntent()
}
startActivityForResult(intent, REQ_REQUEST_MEDIA_PROJECTION)
}
else -> finish()

View File

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

View File

@@ -591,13 +591,7 @@ class _GeneralState extends State<_General> {
isServer: false,
),
],
if (!incomingOnly)
_OptionCheckBox(
context,
'Enable WebRTC P2P connection',
kOptionEnableWebrtc,
isServer: false,
),
if (!incomingOnly) ...webrtcOptions(context),
if (!isWeb && !incomingOnly)
Tooltip(
message: translate('sync-clipboard-between-sessions-tip'),
@@ -887,6 +881,85 @@ class _GeneralState extends State<_General> {
).marginOnly(left: _kContentHMargin);
});
}
// How long an already-connected relay is held back to give the direct WebRTC
// attempt a chance to win. It only means anything while WebRTC is on, so it
// follows the checkbox as an indented sub-option and is hidden outright when
// the box is clear — the shape `directIp` uses for its port.
List<Widget> webrtcOptions(BuildContext context) {
final stored = bind.mainGetLocalOption(key: kOptionRelayFallbackDelay);
final controller = TextEditingController(text: stored);
// What the field holds against what is saved. Apply is offered only while
// the two differ, so an untouched field shows no button at all, and neither
// does one typed back to its saved value or cleared when nothing was saved
// — the state an "edited" flag alone would still call dirty.
final typed = RxString(stored);
final saved = RxString(stored);
return [
_OptionCheckBox(
context,
'Enable WebRTC P2P connection',
kOptionEnableWebrtc,
isServer: false,
update: (_) => setState(() {}),
),
() {
final enabled = mainGetLocalBoolOptionSync(kOptionEnableWebrtc);
final isOptFixed = isOptionFixed(kOptionRelayFallbackDelay);
return Offstage(
offstage: !enabled,
child: Tooltip(
message: translate('relay-fallback-delay-tip'),
child: _SubLabeledWidget(
context,
'Relay fallback delay in seconds',
Row(children: [
SizedBox(
width: 95,
child: TextField(
controller: controller,
enabled: enabled && !isOptFixed,
onChanged: (v) => typed.value = v,
inputFormatters: [
// Seconds, at most one decimal. Clearing the field is
// allowed and restores the built-in default.
FilteringTextInputFormatter.allow(
RegExp(r'^([0-9]|[1-9][0-9])(\.[0-9]?)?$')),
],
decoration: const InputDecoration(
hintText: '2.5',
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
).workaroundFreezeLinuxMint().marginOnly(right: 15),
),
Obx(() => Offstage(
offstage: typed.value.trim() == saved.value.trim(),
child: ElevatedButton(
onPressed: enabled &&
!isOptFixed &&
!typed.value.trim().endsWith('.') &&
double.tryParse(typed.value.trim()) != 0
? () async {
final v = controller.text.trim();
await bind.mainSetLocalOption(
key: kOptionRelayFallbackDelay, value: v);
if (controller.text != v) controller.text = v;
typed.value = v;
saved.value = v;
}
: null,
child: Text(translate('Apply')),
),
))
]),
enabled: enabled && !isOptFixed,
),
),
);
}(),
];
}
}
enum _AccessMode {

View File

@@ -175,7 +175,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
// `on_voice_call_closed` should be called when the connection is ended.
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
// Only one client is considered here for now.
gFFI.chatModel.onVoiceCallClosed("End connetion");
gFFI.chatModel.onVoiceCallClosed("End connection");
}
@override

View File

@@ -141,7 +141,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
// `on_voice_call_closed` should be called when the connection is ended.
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
// Only one client is considered here for now.
gFFI.chatModel.onVoiceCallClosed("End connetion");
gFFI.chatModel.onVoiceCallClosed("End connection");
}
@override

View File

@@ -896,9 +896,13 @@ class FfiModel with ChangeNotifier {
final text = evt['text'];
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 &&
_androidDocumentPickerActive &&
title == 'Connection Error') {
(title == 'Connection Error' ||
(type == 'restarting-show' && title == 'Connecting...'))) {
_androidDocumentPickerInterruptedConnection = true;
return;
}
@@ -3597,9 +3601,11 @@ class QualityMonitorModel with ChangeNotifier {
bool get show => _show;
QualityMonitorData get data => _data;
// Only a WebRTC session names its transport here: web has no session tab
// to show it on, and WebRTC is the one path that can be direct or TURN.
// Only a WebRTC session on the web names its transport here: web has no
// session tab to show it on (the desktop tab's tooltip already does), and
// WebRTC is the one path that can be direct or TURN.
String? get webrtcTransport {
if (!isWeb) return null;
final ffiModel = parent.target?.ffiModel;
if (ffiModel == null) return null;
final streamType = ffiModel.cachedPeerData.streamType;

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:uuid/uuid.dart';
final _sessionId = UuidValue('00000000-0000-0000-0000-000000000000');
class _FakeFFI implements FFI {
@override
UuidValue get sessionId => _sessionId;
@override
late final FfiModel ffiModel = FfiModel(WeakReference(this));
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
test('the quality monitor names the WebRTC transport only on web', () {
final ffi = _FakeFFI();
ffi.ffiModel.cachedPeerData.streamType = 'WebRTC';
final model = QualityMonitorModel(WeakReference(ffi));
// Off the web the session tab's tooltip already names the transport.
expect(isWeb, isFalse);
expect(model.webrtcTransport, isNull);
});
}

View File

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

View File

@@ -41,6 +41,13 @@ impl<T> Drop for ComPtr<T> {
}
}
#[derive(Clone, Copy, PartialEq)]
enum FrameState {
Idle,
Acquired,
Mapped,
}
pub struct Capturer {
device: ComPtr<ID3D11Device>,
display: Display,
@@ -58,6 +65,7 @@ pub struct Capturer {
output_texture: bool,
adapter_desc1: DXGI_ADAPTER_DESC1,
rotate: Rotate,
frame_state: FrameState,
}
impl Capturer {
@@ -174,6 +182,7 @@ impl Capturer {
output_texture: false,
adapter_desc1,
rotate,
frame_state: FrameState::Idle,
})
}
@@ -335,6 +344,7 @@ impl Capturer {
let mut info = mem::MaybeUninit::uninit().assume_init();
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
self.frame_state = FrameState::Acquired;
let frame = ComPtr(frame);
if *info.LastPresentTime.QuadPart() == 0 {
@@ -345,9 +355,11 @@ impl Capturer {
let mut rect = mem::MaybeUninit::uninit().assume_init();
if self.fastlane {
wrap_hresult((*self.duplication.0).MapDesktopSurface(&mut rect))?;
self.frame_state = FrameState::Mapped;
} else {
self.surface = ComPtr(self.ohgodwhat(frame.0)?);
wrap_hresult((*self.surface.0).Map(&mut rect, DXGI_MAP_READ))?;
self.frame_state = FrameState::Mapped;
}
Ok((rect.pBits, rect.Pitch))
}
@@ -424,7 +436,7 @@ impl Capturer {
}
}
} else {
self.unmap();
self.release_frame()?;
let r = self.load_frame(timeout)?;
let rotate = match self.display.rotation() {
DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => kRotate0,
@@ -472,12 +484,13 @@ impl Capturer {
if self.duplication.0.is_null() {
return Err(std::io::ErrorKind::AddrNotAvailable.into());
}
(*self.duplication.0).ReleaseFrame();
self.release_frame()?;
let mut frame = ptr::null_mut();
#[allow(invalid_value)]
let mut info = mem::MaybeUninit::uninit().assume_init();
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
self.frame_state = FrameState::Acquired;
let frame = ComPtr(frame);
if info.AccumulatedFrames == 0 || *info.LastPresentTime.QuadPart() == 0 {
@@ -574,16 +587,42 @@ impl Capturer {
}
}
fn unmap(&self) {
fn release_frame(&mut self) -> io::Result<()> {
if self.duplication.is_null() {
return Ok(());
}
let mut first_error = None;
// Unmap before ReleaseFrame invalidates the desktop surface; use the same
// order for staging surfaces. Cleanup advances Mapped -> Acquired -> Idle,
// while Idle is a no-op. Advance state even on errors to avoid retrying
// cleanup, but still attempt ReleaseFrame if unmapping fails.
unsafe {
(*self.duplication.0).ReleaseFrame();
if self.fastlane {
(*self.duplication.0).UnMapDesktopSurface();
} else {
if !self.surface.is_null() {
(*self.surface.0).Unmap();
if self.frame_state == FrameState::Mapped {
let result = if self.fastlane {
wrap_hresult((*self.duplication.0).UnMapDesktopSurface())
} else if !self.surface.is_null() {
wrap_hresult((*self.surface.0).Unmap())
} else {
Ok(())
};
self.frame_state = FrameState::Acquired;
if let Err(err) = result {
first_error = Some(err);
}
}
if self.frame_state == FrameState::Acquired {
let result = wrap_hresult((*self.duplication.0).ReleaseFrame());
self.frame_state = FrameState::Idle;
if first_error.is_none() {
if let Err(err) = result {
first_error = Some(err);
}
}
}
}
match first_error {
Some(err) => Err(err),
None => Ok(()),
}
}
@@ -599,8 +638,8 @@ impl Capturer {
impl Drop for Capturer {
fn drop(&mut self) {
if !self.duplication.is_null() {
self.unmap();
if let Err(err) = self.release_frame() {
eprintln!("DXGI frame cleanup failed: {err}");
}
}
}

View File

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

View File

@@ -10,7 +10,7 @@ if [ "$1" = configure ]; then
if [ "systemd" == "$INITSYS" ]; then
if [ -e /etc/systemd/system/rustdesk.service ]; then
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
rm -f /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
fi
mkdir -p /usr/lib/systemd/system/
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service

View File

@@ -1,8 +1,10 @@
# FFmpeg 7.1.1 includes the HEVC WPP slice-thread deadlock fix:
# https://github.com/FFmpeg/FFmpeg/commit/79c47dfd25f101b6842bbec8c6ffef8d5077c3ae
vcpkg_from_github(
OUT_SOURCE_PATH SOURCE_PATH
REPO ffmpeg/ffmpeg
REF "n${VERSION}"
SHA512 3b273769ef1a1b63aed0691eef317a760f8c83b1d0e1c232b67bbee26db60b4864aafbc88df0e86d6bebf07185bbd057f33e2d5258fde6d97763b9994cd48b6f
SHA512 6b9a5ee501be41d6abc7579a106263b31f787321cbc45dedee97abf992bf8236cdb2394571dd256a74154f4a20018d429ae7e7f0409611ddc4d6f529d924d175
HEAD_REF master
PATCHES
0001-create-lib-libraries.patch

View File

@@ -1,7 +1,6 @@
{
"name": "ffmpeg",
"version": "7.1",
"port-version": 1,
"version": "7.1.1",
"description": [
"a library to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created.",
"FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure FATE across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations."

242
src/audio_resampler.rs Normal file
View File

@@ -0,0 +1,242 @@
use hbb_common::thiserror;
#[cfg(test)]
pub(crate) mod allocation_tests;
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
mod sinc;
const INTERPOLATION_MARGIN_FRAMES: usize = 2;
const PENDING_PACKET_CAPACITY: usize = 2;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct AudioResamplerConfig {
pub input_rate: u32,
pub output_rate: u32,
pub channels: u16,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub(crate) enum AudioResamplerError {
#[error(
"invalid audio resampler configuration: input_rate={}, output_rate={}, channels={}",
.0.input_rate, .0.output_rate, .0.channels
)]
InvalidConfig(AudioResamplerConfig),
#[error("invalid resampler output frame size: {output_frames}")]
InvalidOutputFrameSize { output_frames: usize },
#[error("audio resampler input length {samples} is not divisible by channel count {channels}")]
IncompleteFrame { samples: usize, channels: usize },
#[error("audio resampler output capacity overflow")]
CapacityOverflow,
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
#[error("audio resampler backend failed: {0}")]
Backend(String),
}
pub(crate) struct FixedFrameAudioResampler {
resampler: AudioResampler,
output_samples: usize,
pending_samples: Vec<f32>,
}
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
// SAFETY: libsamplerate's src_new state owns heap data and has no thread affinity.
// This wrapper never exposes or shares that state; processing requires &mut self.
unsafe impl Send for FixedFrameAudioResampler {}
impl FixedFrameAudioResampler {
pub(crate) fn new(
config: AudioResamplerConfig,
output_frames: usize,
) -> Result<Self, AudioResamplerError> {
if output_frames == 0 {
return Err(AudioResamplerError::InvalidOutputFrameSize { output_frames });
}
let channels = validate_config(config)?;
let output_samples = output_frames
.checked_mul(channels)
.ok_or(AudioResamplerError::CapacityOverflow)?;
let input_frames = output_frames
.checked_mul(config.input_rate as usize)
.ok_or(AudioResamplerError::CapacityOverflow)?
.div_ceil(config.output_rate as usize);
let capacity = output_samples
.checked_mul(PENDING_PACKET_CAPACITY)
.and_then(|samples| samples.checked_add(channels * INTERPOLATION_MARGIN_FRAMES))
.ok_or(AudioResamplerError::CapacityOverflow)?;
let mut resampler = AudioResampler::new(config)?;
resampler.reserve_input(input_frames)?;
Ok(Self {
resampler,
output_samples,
pending_samples: Vec::with_capacity(capacity),
})
}
pub(crate) fn process_with(
&mut self,
input: &[f32],
mut on_packet: impl FnMut(&[f32]),
) -> Result<(), AudioResamplerError> {
self.resampler
.process_into(input, &mut self.pending_samples)?;
let complete_samples =
self.pending_samples.len() / self.output_samples * self.output_samples;
for packet in self.pending_samples[..complete_samples].chunks_exact(self.output_samples) {
on_packet(packet);
}
self.pending_samples.drain(..complete_samples);
Ok(())
}
#[cfg(test)]
pub(crate) fn process(&mut self, input: &[f32]) -> Result<Vec<Vec<f32>>, AudioResamplerError> {
let mut packets = Vec::new();
self.process_with(input, |packet| packets.push(packet.to_owned()))?;
Ok(packets)
}
}
pub(crate) struct AudioResampler {
#[cfg(not(all(feature = "use_samplerate", not(feature = "use_dasp"))))]
backend: StreamingLinearAudioResampler,
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
backend: sinc::SincAudioResampler,
}
impl AudioResampler {
pub(crate) fn new(config: AudioResamplerConfig) -> Result<Self, AudioResamplerError> {
Ok(Self {
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
backend: sinc::SincAudioResampler::new(config)?,
#[cfg(not(all(feature = "use_samplerate", not(feature = "use_dasp"))))]
backend: StreamingLinearAudioResampler::new(config)?,
})
}
pub(crate) fn process(&mut self, input: &[f32]) -> Result<Vec<f32>, AudioResamplerError> {
let mut output = Vec::new();
self.process_into(input, &mut output)?;
Ok(output)
}
// Append samples so capture can retain an incomplete output packet in the same buffer.
fn process_into(
&mut self,
input: &[f32],
output: &mut Vec<f32>,
) -> Result<(), AudioResamplerError> {
self.backend.process_into(input, output)
}
fn reserve_input(&mut self, _frames: usize) -> Result<(), AudioResamplerError> {
#[cfg(not(all(feature = "use_samplerate", not(feature = "use_dasp"))))]
{
let capacity = _frames
.checked_add(INTERPOLATION_MARGIN_FRAMES)
.and_then(|frames| frames.checked_mul(self.backend.channels))
.ok_or(AudioResamplerError::CapacityOverflow)?;
self.backend.buffered_samples.reserve(capacity);
}
Ok(())
}
}
#[cfg(not(all(feature = "use_samplerate", not(feature = "use_dasp"))))]
struct StreamingLinearAudioResampler {
config: AudioResamplerConfig,
channels: usize,
buffered_samples: Vec<f32>,
next_position: u64,
}
#[cfg(not(all(feature = "use_samplerate", not(feature = "use_dasp"))))]
impl StreamingLinearAudioResampler {
fn new(config: AudioResamplerConfig) -> Result<Self, AudioResamplerError> {
Ok(Self {
config,
channels: validate_config(config)?,
buffered_samples: Vec::new(),
next_position: 0,
})
}
fn process_into(
&mut self,
input: &[f32],
output: &mut Vec<f32>,
) -> Result<(), AudioResamplerError> {
validate_input(input, self.channels)?;
let capacity = self.output_capacity(input.len())?;
output.reserve(capacity);
self.buffered_samples.extend_from_slice(input);
while self.write_next_frame(output) {
self.next_position += self.config.input_rate as u64;
}
self.discard_consumed_frames();
Ok(())
}
fn output_capacity(&self, input_samples: usize) -> Result<usize, AudioResamplerError> {
let input_frames = input_samples / self.channels;
let scaled_frames = input_frames
.checked_mul(self.config.output_rate as usize)
.ok_or(AudioResamplerError::CapacityOverflow)?
/ self.config.input_rate as usize;
scaled_frames
.checked_add(INTERPOLATION_MARGIN_FRAMES)
.and_then(|frames| frames.checked_mul(self.channels))
.ok_or(AudioResamplerError::CapacityOverflow)
}
fn write_next_frame(&self, output: &mut Vec<f32>) -> bool {
let output_rate = self.config.output_rate as u64;
let frame_count = self.buffered_samples.len() / self.channels;
let frame = (self.next_position / output_rate) as usize;
let fraction = self.next_position % output_rate;
if frame >= frame_count || (fraction != 0 && frame + 1 >= frame_count) {
return false;
}
let weight = fraction as f32 / output_rate as f32;
for channel in 0..self.channels {
let current = self.buffered_samples[frame * self.channels + channel];
let next_frame = frame + usize::from(fraction != 0);
let next = self.buffered_samples[next_frame * self.channels + channel];
output.push(current + (next - current) * weight);
}
true
}
fn discard_consumed_frames(&mut self) {
let output_rate = self.config.output_rate as u64;
let available_frames = self.buffered_samples.len() / self.channels;
let consumed_frames = ((self.next_position / output_rate) as usize).min(available_frames);
self.buffered_samples
.drain(0..consumed_frames * self.channels);
self.next_position -= consumed_frames as u64 * output_rate;
}
}
fn validate_config(config: AudioResamplerConfig) -> Result<usize, AudioResamplerError> {
if config.input_rate == 0 || config.output_rate == 0 || config.channels == 0 {
return Err(AudioResamplerError::InvalidConfig(config));
}
Ok(config.channels as usize)
}
fn validate_input(input: &[f32], channels: usize) -> Result<(), AudioResamplerError> {
if input.len() % channels != 0 {
return Err(AudioResamplerError::IncompleteFrame {
samples: input.len(),
channels,
});
}
Ok(())
}
#[cfg(all(test, not(all(feature = "use_samplerate", not(feature = "use_dasp")))))]
mod tests;
#[cfg(all(test, feature = "use_samplerate", not(feature = "use_dasp")))]
mod samplerate_tests;

View File

@@ -0,0 +1,142 @@
use super::{AudioResamplerConfig, FixedFrameAudioResampler};
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
struct CountingAllocator;
thread_local! {
static ALLOCATIONS: Cell<Option<usize>> = const { Cell::new(None) };
}
fn record_allocation() {
let _ = ALLOCATIONS.try_with(|count| {
if let Some(value) = count.get() {
count.set(Some(value + 1));
}
});
}
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
record_allocation();
unsafe { System.alloc(layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
record_allocation();
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
record_allocation();
unsafe { System.realloc(ptr, layout, size) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
pub(crate) fn assert_no_allocations(process: impl FnOnce()) {
struct ResetCounter;
impl Drop for ResetCounter {
fn drop(&mut self) {
ALLOCATIONS.with(|count| count.set(None));
}
}
ALLOCATIONS.with(|count| assert!(count.replace(Some(0)).is_none()));
let reset = ResetCounter;
process();
let allocations = ALLOCATIONS.with(|count| count.get().unwrap());
drop(reset);
assert_eq!(
allocations, 0,
"PCM processing allocated on the capture thread"
);
}
#[test]
fn capture_resampling_reuses_buffers() {
const PACKETS_PER_SECOND: usize = 100;
const PACKET_COUNT: usize = 100;
const MAX_STARTUP_DELAY_PACKETS: usize = 1;
const SIGNAL_LEVEL: f32 = 0.25;
const RATE_PAIRS: [(u32, u32); 6] = [
(32_000, 24_000),
(44_100, 24_000),
(44_100, 48_000),
(48_000, 24_000),
(96_000, 48_000),
(192_000, 48_000),
];
for (input_rate, output_rate) in RATE_PAIRS {
for channels in [1, 2, 4, 6, 8] {
let config = AudioResamplerConfig {
input_rate,
output_rate,
channels,
};
let input =
vec![SIGNAL_LEVEL; input_rate as usize / PACKETS_PER_SECOND * channels as usize];
let frames = output_rate as usize / PACKETS_PER_SECOND;
let mut resampler = FixedFrameAudioResampler::new(config, frames).unwrap();
let mut packets = 0;
let mut energy = 0.0;
assert_no_allocations(|| {
for _ in 0..PACKET_COUNT {
resampler
.process_with(&input, |packet| {
assert_eq!(packet.len(), frames * channels as usize);
energy += packet.iter().map(|sample| sample * sample).sum::<f32>();
packets += 1;
})
.unwrap();
}
});
assert!((PACKET_COUNT - MAX_STARTUP_DELAY_PACKETS..=PACKET_COUNT).contains(&packets));
assert!(energy > SIGNAL_LEVEL);
}
}
}
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
#[test]
fn sinc_output_matches_the_existing_backend() {
use super::AudioResampler;
const INPUT_FRAMES: usize = 2_048;
const CHUNK_FRAMES: usize = 73;
const SIGNAL_STEP: f32 = 0.07;
for (input_rate, output_rate) in [(44_100, 24_000), (44_100, 48_000), (96_000, 48_000)] {
for channels in [1, 2, 4, 6, 8] {
let config = AudioResamplerConfig {
input_rate,
output_rate,
channels,
};
let input: Vec<_> = (0..INPUT_FRAMES * channels as usize)
.map(|sample| (sample as f32 * SIGNAL_STEP).sin())
.collect();
let mut actual = AudioResampler::new(config).unwrap();
let expected = samplerate::Samplerate::new(
samplerate::ConverterType::SincBestQuality,
input_rate,
output_rate,
channels as usize,
)
.unwrap();
for chunk in input.chunks(CHUNK_FRAMES * channels as usize) {
assert_eq!(
actual.process(chunk).unwrap(),
expected.process(chunk).unwrap()
);
assert_eq!(actual.process(&[]).unwrap(), expected.process(&[]).unwrap());
}
}
}
}

View File

@@ -0,0 +1,157 @@
use super::{AudioResampler, AudioResamplerConfig, AudioResamplerError, FixedFrameAudioResampler};
const INPUT_RATE: u32 = 44_100;
const OUTPUT_RATE: u32 = 48_000;
const CHANNELS: u16 = 2;
const INPUT_PACKET_FRAMES: usize = INPUT_RATE as usize / PACKETS_PER_SECOND;
const OUTPUT_PACKET_FRAMES: usize = OUTPUT_RATE as usize / PACKETS_PER_SECOND;
const PACKET_COUNT: usize = 20;
const PACKETS_PER_SECOND: usize = 100;
const MIN_CONTINUITY_PACKETS: usize = 2;
const TONE_FREQUENCY_HZ: f32 = 997.0;
const TONE_AMPLITUDE: f32 = 0.5;
const MAX_BOUNDARY_RESIDUAL: f32 = 0.02;
const INCOMPLETE_SAMPLE_COUNT: usize = 1;
const DOWNSAMPLE_RATE: u32 = 24_000;
const REJECTED_TONE_HZ: f64 = 18_000.0;
const MAX_ALIAS_RMS: f64 = 0.01;
const MIN_PASSBAND_RMS: f64 = 0.3;
fn stereo_tone(frames: usize) -> Vec<f32> {
(0..frames)
.flat_map(|frame| {
let phase =
std::f32::consts::TAU * TONE_FREQUENCY_HZ * frame as f32 / INPUT_RATE as f32;
let sample = TONE_AMPLITUDE * phase.sin();
[sample, sample]
})
.collect()
}
fn maximum_boundary_residual(packets: &[Vec<f32>]) -> f32 {
packets.windows(2).fold(0.0, |maximum, pair| {
let previous = &pair[0];
let current = &pair[1];
let last = previous.len() - CHANNELS as usize;
let penultimate = last - CHANNELS as usize;
(0..CHANNELS as usize).fold(maximum, |maximum, channel| {
let predicted = previous[last + channel]
+ (previous[last + channel] - previous[penultimate + channel]);
maximum.max((current[channel] - predicted).abs())
})
})
}
fn stereo_config() -> AudioResamplerConfig {
AudioResamplerConfig {
input_rate: INPUT_RATE,
output_rate: OUTPUT_RATE,
channels: CHANNELS,
}
}
#[test]
fn moving_capture_resampler_preserves_pending_audio() {
let input = stereo_tone(INPUT_PACKET_FRAMES * PACKET_COUNT);
let packet_samples = INPUT_PACKET_FRAMES * CHANNELS as usize;
let mut expected_resampler =
FixedFrameAudioResampler::new(stereo_config(), OUTPUT_PACKET_FRAMES).unwrap();
let expected: Vec<_> = input
.chunks(packet_samples)
.flat_map(|packet| expected_resampler.process(packet).unwrap())
.collect();
let mut moved_resampler =
FixedFrameAudioResampler::new(stereo_config(), OUTPUT_PACKET_FRAMES).unwrap();
let mut output = moved_resampler.process(&input[..packet_samples]).unwrap();
let remaining = std::thread::spawn(move || {
input[packet_samples..]
.chunks(packet_samples)
.flat_map(|packet| moved_resampler.process(packet).unwrap())
.collect::<Vec<_>>()
})
.join()
.unwrap();
output.extend(remaining);
assert!(output.len() >= MIN_CONTINUITY_PACKETS);
assert!(output
.iter()
.all(|packet| packet.len() == OUTPUT_PACKET_FRAMES * CHANNELS as usize));
assert!(maximum_boundary_residual(&output) <= MAX_BOUNDARY_RESIDUAL);
assert_eq!(output, expected);
}
fn downsampled_rms(input: &[f32]) -> f64 {
let config = AudioResamplerConfig {
output_rate: DOWNSAMPLE_RATE,
..stereo_config()
};
let output_frames = DOWNSAMPLE_RATE as usize / PACKETS_PER_SECOND;
let mut resampler = FixedFrameAudioResampler::new(config, output_frames).unwrap();
let output: Vec<f32> = input
.chunks(INPUT_PACKET_FRAMES * CHANNELS as usize)
.flat_map(|packet| resampler.process(packet).unwrap().into_iter().flatten())
.collect();
assert!(output.len() >= output_frames * CHANNELS as usize * MIN_CONTINUITY_PACKETS);
let mean_square = output
.iter()
.map(|sample| f64::from(*sample).powi(2))
.sum::<f64>()
/ output.len() as f64;
mean_square.sqrt()
}
#[test]
fn capture_downsampling_filters_out_of_band_audio() {
let input: Vec<_> = (0..INPUT_PACKET_FRAMES * PACKET_COUNT)
.flat_map(|frame| {
let phase =
std::f64::consts::TAU * REJECTED_TONE_HZ * frame as f64 / f64::from(INPUT_RATE);
let sample = (f64::from(TONE_AMPLITUDE) * phase.sin()) as f32;
[sample, sample]
})
.collect();
let rms = downsampled_rms(&input);
assert!(
rms < MAX_ALIAS_RMS,
"out-of-band output RMS {rms} exceeded {MAX_ALIAS_RMS}"
);
let input = stereo_tone(INPUT_PACKET_FRAMES * PACKET_COUNT);
let rms = downsampled_rms(&input);
assert!(
rms > MIN_PASSBAND_RMS,
"in-band output RMS {rms} fell below {MIN_PASSBAND_RMS}"
);
}
#[test]
fn samplerate_backend_preserves_streaming_continuity() {
let input = stereo_tone(INPUT_PACKET_FRAMES * PACKET_COUNT);
let mut resampler = AudioResampler::new(stereo_config()).unwrap();
let packets: Vec<_> = input
.chunks(INPUT_PACKET_FRAMES * CHANNELS as usize)
.map(|packet| resampler.process(packet).unwrap())
.filter(|packet| !packet.is_empty())
.collect();
assert!(packets.len() >= MIN_CONTINUITY_PACKETS);
assert!(packets
.iter()
.all(|packet| packet.len() % CHANNELS as usize == 0));
assert!(maximum_boundary_residual(&packets) <= MAX_BOUNDARY_RESIDUAL);
}
#[test]
fn samplerate_backend_reports_incomplete_frame_context() {
let mut resampler = AudioResampler::new(stereo_config()).unwrap();
assert_eq!(
resampler.process(&[0.0]).unwrap_err(),
AudioResamplerError::IncompleteFrame {
samples: INCOMPLETE_SAMPLE_COUNT,
channels: CHANNELS as usize,
}
);
}

112
src/audio_resampler/sinc.rs Normal file
View File

@@ -0,0 +1,112 @@
use super::{AudioResamplerConfig, AudioResamplerError};
use libsamplerate_sys as sys;
use std::ptr::NonNull;
const OUTPUT_MARGIN_FRAMES: usize = 1;
pub(super) struct SincAudioResampler {
state: NonNull<sys::SRC_STATE>,
config: AudioResamplerConfig,
}
impl SincAudioResampler {
pub(super) fn new(config: AudioResamplerConfig) -> Result<Self, AudioResamplerError> {
super::validate_config(config)?;
let ratio = f64::from(config.output_rate) / f64::from(config.input_rate);
if unsafe { sys::src_is_valid_ratio(ratio) } == 0 {
return Err(backend_error(
config,
samplerate::ErrorCode::BadSrcRatio as _,
));
}
let mut error = 0;
// SAFETY: src_new allocates independent state; this owner releases it in Drop.
let state = unsafe {
sys::src_new(
sys::SRC_SINC_BEST_QUALITY as _,
config.channels.into(),
&mut error,
)
};
let state = NonNull::new(state).ok_or_else(|| backend_error(config, error))?;
Ok(Self { state, config })
}
pub(super) fn process_into(
&mut self,
input: &[f32],
output: &mut Vec<f32>,
) -> Result<(), AudioResamplerError> {
super::validate_input(input, self.config.channels as usize)?;
let mut consumed = 0;
loop {
let (used, generated) = self.process_block(&input[consumed..], output)?;
consumed += used;
if consumed == input.len() {
return Ok(());
}
if used == 0 && generated == 0 {
return Err(AudioResamplerError::Backend(
"libsamplerate made no progress while input remained".to_owned(),
));
}
}
}
fn process_block(
&mut self,
input: &[f32],
output: &mut Vec<f32>,
) -> Result<(usize, usize), AudioResamplerError> {
let channels = self.config.channels as usize;
let input_frames = input.len() / channels;
let output_frames = input_frames
.checked_mul(self.config.output_rate as usize)
.map(|frames| frames / self.config.input_rate as usize)
.and_then(|frames| frames.checked_add(OUTPUT_MARGIN_FRAMES))
.ok_or(AudioResamplerError::CapacityOverflow)?;
let start = output.len();
let end = output_frames
.checked_mul(channels)
.and_then(|samples| start.checked_add(samples))
.ok_or(AudioResamplerError::CapacityOverflow)?;
let mut data = sys::SRC_DATA {
data_in: input.as_ptr(),
input_frames: input_frames
.try_into()
.map_err(|_| AudioResamplerError::CapacityOverflow)?,
output_frames: output_frames
.try_into()
.map_err(|_| AudioResamplerError::CapacityOverflow)?,
src_ratio: f64::from(self.config.output_rate) / f64::from(self.config.input_rate),
..Default::default()
};
output.resize(end, 0.0);
data.data_out = output[start..].as_mut_ptr();
// SAFETY: state is exclusively owned; disjoint slices cover the declared frame counts.
let error = unsafe { sys::src_process(self.state.as_ptr(), &mut data) };
let generated = data.output_frames_gen as usize * channels;
output.truncate(start + generated);
if error != 0 {
return Err(backend_error(self.config, error));
}
Ok((data.input_frames_used as usize * channels, generated))
}
}
impl Drop for SincAudioResampler {
fn drop(&mut self) {
// SAFETY: this owner holds the only handle returned by src_new.
unsafe { sys::src_delete(self.state.as_ptr()) };
}
}
fn backend_error(config: AudioResamplerConfig, code: i32) -> AudioResamplerError {
AudioResamplerError::Backend(format!(
"input_rate={}, output_rate={}, channels={}: {:?}",
config.input_rate,
config.output_rate,
config.channels,
samplerate::Error::from_int(code)
))
}

View File

@@ -0,0 +1,178 @@
use super::{AudioResampler, AudioResamplerConfig, FixedFrameAudioResampler};
const INPUT_RATE: u32 = 24_000;
const OUTPUT_RATE: u32 = 48_000;
const CHANNELS: u16 = 2;
const CHUNK_FRAMES: usize = 240;
const CHUNK_COUNT: usize = 4;
const TONE_FREQUENCY_HZ: f32 = 997.0;
const TONE_AMPLITUDE: f32 = 0.5;
const MAX_BOUNDARY_RESIDUAL: f32 = 0.02;
const LOOK_AHEAD_OUTPUT_FRAMES: usize = 1;
const UNEVEN_CHUNK_FRAMES: usize = 73;
const MONO_CHANNELS: u16 = 1;
const UNIT_RATE: u32 = 1;
const DOUBLE_RATE: u32 = 2;
const FIRST_DOWNSAMPLE_PACKET: [f32; 3] = [0.0, 1.0, 2.0];
const SECOND_DOWNSAMPLE_PACKET: [f32; 4] = [3.0, 4.0, 5.0, 6.0];
const EXPECTED_DOWNSAMPLED_OUTPUT: [f32; 4] = [0.0, 2.0, 4.0, 6.0];
const PACKETS_PER_SECOND: usize = 100;
const OUTPUT_PACKET_FRAMES: usize = OUTPUT_RATE as usize / PACKETS_PER_SECOND;
const RATE_44_1_KHZ: u32 = 44_100;
const FLOAT_TOLERANCE: f32 = 0.000_001;
const MIN_CONTINUITY_PACKETS: usize = 2;
fn stereo_tone_at_rate(frames: usize, sample_rate: u32) -> Vec<f32> {
(0..frames)
.flat_map(|frame| {
let phase =
std::f32::consts::TAU * TONE_FREQUENCY_HZ * frame as f32 / sample_rate as f32;
let sample = TONE_AMPLITUDE * phase.sin();
[sample, sample]
})
.collect()
}
fn stereo_tone(frames: usize) -> Vec<f32> {
stereo_tone_at_rate(frames, INPUT_RATE)
}
fn maximum_tone_prediction_residual(sample_rate: u32) -> f32 {
let half_step = std::f32::consts::PI * TONE_FREQUENCY_HZ / sample_rate as f32;
4.0 * TONE_AMPLITUDE * half_step.sin().powi(2)
}
fn maximum_boundary_residual(chunks: &[Vec<f32>]) -> f32 {
chunks.windows(2).fold(0.0, |maximum, pair| {
let previous = &pair[0];
let current = &pair[1];
let last = previous.len() - CHANNELS as usize;
let penultimate = last - CHANNELS as usize;
(0..CHANNELS as usize).fold(maximum, |maximum, channel| {
let predicted = previous[last + channel]
+ (previous[last + channel] - previous[penultimate + channel]);
maximum.max((current[channel] - predicted).abs())
})
})
}
fn stereo_config() -> AudioResamplerConfig {
AudioResamplerConfig {
input_rate: INPUT_RATE,
output_rate: OUTPUT_RATE,
channels: CHANNELS,
}
}
#[test]
fn preserves_decoded_packet_continuity_and_output_ratio() {
let input = stereo_tone(CHUNK_FRAMES * CHUNK_COUNT);
let mut whole_resampler = AudioResampler::new(stereo_config()).unwrap();
let whole_output = whole_resampler.process(&input).unwrap();
let expected_frames = CHUNK_FRAMES * CHUNK_COUNT * OUTPUT_RATE as usize / INPUT_RATE as usize
- LOOK_AHEAD_OUTPUT_FRAMES;
for chunk_frames in [CHUNK_FRAMES, UNEVEN_CHUNK_FRAMES] {
let mut resampler = AudioResampler::new(stereo_config()).unwrap();
let output: Vec<_> = input
.chunks(chunk_frames * CHANNELS as usize)
.map(|chunk| resampler.process(chunk).unwrap())
.collect();
let residual = maximum_boundary_residual(&output);
assert!(
residual <= MAX_BOUNDARY_RESIDUAL,
"packet boundary residual {residual} exceeded {MAX_BOUNDARY_RESIDUAL}, chunk_frames={chunk_frames}"
);
let output_frames = output.iter().map(Vec::len).sum::<usize>() / CHANNELS as usize;
assert_eq!(output_frames, expected_frames);
assert_eq!(output.concat(), whole_output, "chunk_frames={chunk_frames}");
}
}
#[test]
fn rejects_incomplete_interleaved_frames() {
let mut resampler = AudioResampler::new(stereo_config()).unwrap();
assert!(resampler.process(&[TONE_AMPLITUDE]).is_err());
}
#[test]
fn interpolates_mono_samples() {
let config = AudioResamplerConfig {
input_rate: UNIT_RATE,
output_rate: DOUBLE_RATE,
channels: MONO_CHANNELS,
};
let mut resampler = AudioResampler::new(config).unwrap();
assert_eq!(
resampler.process(&[0.0, 1.0, 2.0]).unwrap(),
[0.0, 0.5, 1.0, 1.5, 2.0]
);
}
#[test]
fn rejects_zero_rate_configuration() {
let config = AudioResamplerConfig {
input_rate: 0,
output_rate: OUTPUT_RATE,
channels: CHANNELS,
};
assert!(AudioResampler::new(config).is_err());
}
#[test]
fn downsamples_across_packet_boundaries() {
let config = AudioResamplerConfig {
input_rate: DOUBLE_RATE,
output_rate: UNIT_RATE,
channels: MONO_CHANNELS,
};
let mut resampler = AudioResampler::new(config).unwrap();
let mut output = resampler.process(&FIRST_DOWNSAMPLE_PACKET).unwrap();
output.extend(resampler.process(&SECOND_DOWNSAMPLE_PACKET).unwrap());
assert_eq!(output, EXPECTED_DOWNSAMPLED_OUTPUT);
}
#[test]
fn sender_resampler_emits_only_complete_continuous_frames() {
let input = stereo_tone(CHUNK_FRAMES * CHUNK_COUNT);
let mut resampler =
FixedFrameAudioResampler::new(stereo_config(), OUTPUT_PACKET_FRAMES).unwrap();
let output: Vec<_> = input
.chunks(CHUNK_FRAMES * CHANNELS as usize)
.flat_map(|chunk| resampler.process(chunk).unwrap())
.collect();
assert!(output.len() >= MIN_CONTINUITY_PACKETS);
assert!(output
.iter()
.all(|packet| packet.len() == OUTPUT_PACKET_FRAMES * CHANNELS as usize));
assert!(maximum_boundary_residual(&output) <= MAX_BOUNDARY_RESIDUAL);
}
#[test]
fn sender_downsampling_preserves_packet_continuity() {
let input_packet_frames = RATE_44_1_KHZ as usize / PACKETS_PER_SECOND;
let input = stereo_tone_at_rate(input_packet_frames * CHUNK_COUNT, RATE_44_1_KHZ);
let config = AudioResamplerConfig {
input_rate: RATE_44_1_KHZ,
output_rate: INPUT_RATE,
channels: CHANNELS,
};
let mut resampler =
FixedFrameAudioResampler::new(config, INPUT_RATE as usize / PACKETS_PER_SECOND).unwrap();
let packets: Vec<_> = input
.chunks(input_packet_frames * CHANNELS as usize)
.flat_map(|packet| resampler.process(packet).unwrap())
.collect();
let residual = maximum_boundary_residual(&packets);
assert_eq!(packets.len(), CHUNK_COUNT);
assert!(
residual <= maximum_tone_prediction_residual(INPUT_RATE) + FLOAT_TOLERANCE,
"sender packet boundary residual {residual} exceeded the tone curvature"
);
}

View File

@@ -93,6 +93,13 @@ use crate::ui_session_interface::SessionPermissionConfig;
pub use super::lang::*;
#[cfg(not(target_os = "linux"))]
mod audio_playback;
#[cfg(target_os = "windows")]
mod audio_playback_recovery;
#[cfg(all(test, not(target_os = "linux")))]
#[path = "client/tests/audio_state_tests.rs"]
mod audio_state_tests;
pub mod file_trait;
pub mod helper;
pub mod io_loop;
@@ -569,7 +576,7 @@ impl Client {
return race_transports_prefer_webrtc(
preferred_fut,
vec![fallback_fut],
Self::WEBRTC_PREFER_WINDOW_MS,
Self::relay_fallback_delay_ms(),
|result| result.0 .1,
)
.await;
@@ -610,11 +617,27 @@ impl Client {
/// ones that traverse NAT.
const MAX_PENDING_WEBRTC_ICE: usize = 64;
/// Prefer-P2P window: how long a WebRTC attempt outranks an already-established relay
/// result, and the floor for a punch-path WebRTC attempt whose race timeout is tuned for a
/// raw TCP SYN. Long enough for candidate trickle + ICE checks + DTLS on high-latency
/// links; short enough that UDP-blocked networks settle on relay without a noticeable wait.
const WEBRTC_PREFER_WINDOW_MS: u64 = 2500;
/// Default relay fallback delay: how long an already-established relay result is held back
/// while a WebRTC attempt is still in flight, and the floor for a punch-path WebRTC attempt
/// whose race timeout is tuned for a raw TCP SYN. Long enough for candidate trickle + ICE
/// checks + DTLS on high-latency links; short enough that UDP-blocked networks settle on
/// relay without a noticeable wait. The same role RFC 8305 calls a connection attempt delay.
const RELAY_FALLBACK_DELAY_MS: u64 = 2500;
/// The delay as the user configured it, falling back to `RELAY_FALLBACK_DELAY_MS`. The
/// settings field holds seconds, which is what a user reasons about; everything here is
/// milliseconds. Unparseable, zero or negative all mean "unset", so clearing the field
/// restores the default instead of collapsing the delay and handing every race to the
/// relay.
fn relay_fallback_delay_ms() -> u64 {
match LocalConfig::get_option(keys::OPTION_RELAY_FALLBACK_DELAY)
.trim()
.parse::<f64>()
{
Ok(secs) if secs.is_finite() && secs > 0.0 => (secs * 1000.0).round() as u64,
_ => Self::RELAY_FALLBACK_DELAY_MS,
}
}
/// UDP-NAT-test wait when the TCP clock is implausible (see TCP_RTT_PLAUSIBLE_MIN). The
/// normal bound is `rtt / 2`: the test has been running since before the TCP connect, so on
@@ -1114,7 +1137,7 @@ impl Client {
race_transports_prefer_webrtc(
webrtc_fut,
connect_futures,
Self::WEBRTC_PREFER_WINDOW_MS,
Self::relay_fallback_delay_ms(),
|result| result.3,
)
.await
@@ -1442,7 +1465,7 @@ impl Client {
// so a viable P2P path is not abandoned before it can complete; TCP/UDP keep the
// tighter timeout, so a working direct connection still wins immediately, and the
// relay fallback only waits the extra time when direct attempts all failed.
let webrtc_timeout = connect_timeout.max(Self::WEBRTC_PREFER_WINDOW_MS);
let webrtc_timeout = connect_timeout.max(Self::relay_fallback_delay_ms());
async move {
raced.wait_connected(webrtc_timeout).await?;
// Resolve the pair here: a TURN win is relayed, not direct, and must be held
@@ -1460,7 +1483,7 @@ impl Client {
race_transports_prefer_webrtc(
webrtc_fut,
direct_futures,
Self::WEBRTC_PREFER_WINDOW_MS,
Self::relay_fallback_delay_ms(),
|r| r.3,
)
.await
@@ -2053,6 +2076,8 @@ pub struct AudioHandler {
simple: Option<psimple::Simple>,
#[cfg(not(target_os = "linux"))]
audio_buffer: AudioBuffer,
#[cfg(not(target_os = "linux"))]
audio_resampler: Option<crate::audio_resampler::AudioResampler>,
sample_rate: (u32, u32),
#[cfg(not(target_os = "linux"))]
audio_stream: Option<Box<dyn StreamTrait>>,
@@ -2060,7 +2085,57 @@ pub struct AudioHandler {
#[cfg(not(target_os = "linux"))]
device_channel: u16,
#[cfg(not(target_os = "linux"))]
ready: Arc<std::sync::Mutex<bool>>,
playback_status: Arc<audio_playback::AudioPlaybackStatus>,
#[cfg(target_os = "windows")]
playback_recovery: audio_playback_recovery::PlaybackRecovery,
}
#[cfg(not(target_os = "linux"))]
#[derive(Clone, Copy)]
struct DecodedAudioConfig {
sample_rate: u32,
input_channels: u16,
output_channels: u16,
}
#[cfg(not(target_os = "linux"))]
fn create_audio_resampler(
input_rate: u32,
output_rate: u32,
channels: u16,
) -> ResultType<Option<crate::audio_resampler::AudioResampler>> {
if input_rate == output_rate {
return Ok(None);
}
Ok(Some(crate::audio_resampler::AudioResampler::new(
crate::audio_resampler::AudioResamplerConfig {
input_rate,
output_rate,
channels,
},
)?))
}
#[cfg(not(target_os = "linux"))]
fn prepare_decoded_audio(
input: &[f32],
resampler: Option<&mut crate::audio_resampler::AudioResampler>,
config: DecodedAudioConfig,
) -> Result<Vec<f32>, crate::audio_resampler::AudioResamplerError> {
let mut output = match resampler {
Some(resampler) => resampler.process(input)?,
None => input.to_owned(),
};
if config.input_channels != config.output_channels {
output = crate::audio_rechannel(
output,
config.sample_rate,
config.sample_rate,
config.input_channels,
config.output_channels,
);
}
Ok(output)
}
#[cfg(not(target_os = "linux"))]
@@ -2068,6 +2143,7 @@ struct AudioBuffer(
pub Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>,
usize,
[usize; 30],
Arc<std::sync::atomic::AtomicUsize>,
);
#[cfg(not(target_os = "linux"))]
@@ -2079,6 +2155,7 @@ impl Default for AudioBuffer {
)),
48000 * 2,
[0; 30],
Arc::new(std::sync::atomic::AtomicUsize::new(0)),
)
}
}
@@ -2153,27 +2230,38 @@ impl AudioBuffer {
let skip = (cap * max / (30 * N) + 1) & (!1);
if (having > skip * 3) && (skip > 0) {
lock.skip(skip);
log::info!("skip {skip}, based {max} {zero}");
let generation = self.signal_discontinuity();
drop(lock);
log::info!("skip {skip}, based {max} {zero}, generation={generation}");
}
}
/// The caller must hold the PCM buffer lock while signaling the discard.
fn signal_discontinuity(&self) -> usize {
self.3
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
.wrapping_add(1)
}
/// append pcm to audio buffer, if buffered data
/// exceeds AUDIO_BUFFER_MS, only AUDIO_BUFFER_MS
/// will be kept.
fn append_pcm2(&self, buffer: &[f32]) -> usize {
let mut lock = self.0.lock().unwrap();
let cap = lock.capacity();
if buffer.len() > cap {
lock.push_slice_overwrite(buffer);
return cap;
}
let having = lock.occupied_len() + buffer.len();
if having > cap {
lock.skip(having - cap);
}
lock.push_slice_overwrite(buffer);
lock.occupied_len()
let discard = (having > cap).then(|| (having - cap, self.signal_discontinuity()));
let occupied = lock.occupied_len();
drop(lock);
if let Some((discarded, generation)) = discard {
hbb_common::throttled_log!(
audio_playback::AUDIO_PLAYBACK_LOG_INTERVAL,
debug,
"Audio buffer capacity discard: samples={discarded}, generation={generation}"
);
}
occupied
}
/// append pcm to audio buffer, trying to drop data
@@ -2185,6 +2273,41 @@ impl AudioBuffer {
}
}
#[cfg(all(test, not(target_os = "linux")))]
mod audio_buffer_discontinuity_tests {
use super::AudioBuffer;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
};
const BUFFER_CAPACITY: usize = 4;
const BUFFER_LEVELS: usize = 30;
const FIRST_INPUT: [f32; 2] = [0.1, 0.2];
const OVERFLOWING_INPUT: [f32; 3] = [0.3, 0.4, 0.5];
const OVERSIZED_INPUT: [f32; 5] = [0.6, 0.7, 0.8, 0.9, 1.0];
#[test]
fn capacity_discards_signal_discontinuities() {
let audio_buffer = AudioBuffer(
Arc::new(Mutex::new(ringbuf::HeapRb::new(BUFFER_CAPACITY))),
BUFFER_CAPACITY,
[0; BUFFER_LEVELS],
Arc::new(AtomicUsize::new(0)),
);
assert_eq!(audio_buffer.append_pcm2(&FIRST_INPUT), FIRST_INPUT.len());
assert_eq!(audio_buffer.3.load(Ordering::Relaxed), 0);
assert_eq!(
audio_buffer.append_pcm2(&OVERFLOWING_INPUT),
BUFFER_CAPACITY
);
assert_eq!(audio_buffer.3.load(Ordering::Relaxed), 1);
assert_eq!(audio_buffer.append_pcm2(&OVERSIZED_INPUT), BUFFER_CAPACITY);
assert_eq!(audio_buffer.3.load(Ordering::Relaxed), 2);
}
}
impl AudioHandler {
#[cfg(target_os = "linux")]
fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> {
@@ -2231,13 +2354,16 @@ impl AudioHandler {
log::info!("Remote input format: {:?}", format0);
#[allow(unused_mut)]
let mut config: StreamConfig = config.into();
#[cfg(not(target_os = "ios"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
// this makes ios audio output not work
// this makes ios and android audio output not work
config.buffer_size = cpal::BufferSize::Fixed(64);
}
self.sample_rate = (format0.sample_rate, config.sample_rate.0);
let audio_resampler = create_audio_resampler(
format0.sample_rate, config.sample_rate.0, format0.channels as _,
)?;
let mut build_output_stream = |config: StreamConfig| match sample_format {
cpal::SampleFormat::I8 => self.build_output_stream::<i8>(&config, &device),
cpal::SampleFormat::I16 => self.build_output_stream::<i16>(&config, &device),
@@ -2262,22 +2388,61 @@ impl AudioHandler {
} else {
build_output_stream(config)?;
}
self.audio_resampler = audio_resampler;
Ok(())
}
/// Handle audio format and create an audio decoder.
pub fn handle_format(&mut self, f: AudioFormat) {
self.handle_format_with_start(f, Self::start_audio);
}
fn handle_format_with_start(
&mut self,
f: AudioFormat,
start: impl FnOnce(&mut Self, AudioFormat) -> ResultType<()>,
) {
if !is_supported_audio_channel_count(f.channels) {
log::error!("Unsupported audio channel count: {}", f.channels);
return;
}
match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) {
Ok(d) => {
#[cfg(target_os = "windows")]
let playback_failed = self.cancel_pending_playback();
#[cfg(target_os = "linux")]
let keep_existing_stream = self.simple.is_some()
&& self.sample_rate.0 == f.sample_rate
&& u32::from(self.channels) == f.channels;
#[cfg(not(target_os = "linux"))]
let keep_existing_stream = self.audio_stream.is_some()
&& self.sample_rate.0 == f.sample_rate
&& u32::from(self.channels) == f.channels;
let buffer = vec![0.; f.sample_rate as usize * f.channels as usize];
#[cfg(not(target_os = "linux"))]
let mut previous = std::mem::take(self);
#[cfg(target_os = "windows")]
self.prepare_playback(&f);
self.audio_decoder = Some((d, buffer));
self.channels = f.channels as _;
allow_err!(self.start_audio(f));
let result = start(self, f);
#[cfg(target_os = "windows")]
let keep_existing_stream = keep_existing_stream
&& !playback_failed
&& !previous.playback_recovery.report_pending();
#[cfg(not(target_os = "linux"))]
if result.is_err() && keep_existing_stream {
// The restarted capture has new Opus history even when output startup fails.
previous.audio_decoder = self.audio_decoder.take();
*self = previous;
self.handle_audio_start_result(result, true);
return;
}
#[cfg(target_os = "windows")]
self.finish_playback_replacement(result, keep_existing_stream.then_some(previous));
#[cfg(not(target_os = "windows"))]
self.handle_audio_start_result(result, keep_existing_stream);
}
Err(err) => {
log::error!("Failed to create audio decoder: {}", err);
@@ -2285,52 +2450,73 @@ impl AudioHandler {
}
}
fn handle_audio_start_result(&mut self, result: ResultType<()>, keep_existing_stream: bool) {
if let Err(error) = result {
if keep_existing_stream {
log::error!(
"Failed to replace audio playback stream; keeping the existing compatible stream: {error:#}"
);
} else {
*self = Self::default();
log::error!("Failed to start audio playback: {error:#}");
}
}
}
/// Handle audio frame and play it.
#[inline]
pub fn handle_frame(&mut self, frame: AudioFrame) {
#[cfg(not(target_os = "linux"))]
if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() {
self.playback_status.report_errors();
#[cfg(not(target_os = "linux"))]
if self.audio_stream.is_none()
|| !self
.playback_status
.ready
.load(std::sync::atomic::Ordering::Acquire)
{
return;
}
#[cfg(target_os = "linux")]
if self.simple.is_none() {
log::debug!("PulseAudio simple binding does not exists");
log::trace!("PulseAudio simple binding does not exists");
return;
}
self.audio_decoder.as_mut().map(|(d, buffer)| {
if let Ok(n) = d.decode_float(&frame.data, buffer, false) {
let channels = self.channels;
let n = n * (channels as usize);
#[cfg(not(target_os = "linux"))]
{
let sample_rate0 = self.sample_rate.0;
let sample_rate = self.sample_rate.1;
let mut buffer = buffer[0..n].to_owned();
if sample_rate != sample_rate0 {
buffer = crate::audio_resample(
&buffer[0..n],
sample_rate0,
sample_rate,
channels,
);
}
if self.channels != self.device_channel {
buffer = crate::audio_rechannel(
buffer,
sample_rate,
sample_rate,
self.channels,
self.device_channel,
);
}
self.audio_buffer.append_pcm(&buffer);
}
#[cfg(target_os = "linux")]
{
let data_u8 =
unsafe { std::slice::from_raw_parts::<u8>(buffer.as_ptr() as _, n * 4) };
self.simple.as_mut().map(|x| x.write(data_u8));
let decoded_frames = match d.decode_float(&frame.data, buffer, false) {
Ok(decoded_frames) => decoded_frames,
Err(error) => {
log::warn!("Failed to decode audio frame: {error:?}");
return;
}
};
let channels = self.channels;
let n = decoded_frames * channels as usize;
#[cfg(not(target_os = "linux"))]
{
let config = DecodedAudioConfig {
sample_rate: self.sample_rate.1,
input_channels: self.channels,
output_channels: self.device_channel,
};
let buffer = match prepare_decoded_audio(
&buffer[0..n],
self.audio_resampler.as_mut(),
config,
) {
Ok(output) => output,
Err(error) => {
log::error!("Failed to resample decoded audio: {error:#}");
return;
}
};
self.audio_buffer.append_pcm(&buffer);
}
#[cfg(target_os = "linux")]
{
let data_u8 =
unsafe { std::slice::from_raw_parts::<u8>(buffer.as_ptr() as _, n * 4) };
self.simple.as_mut().map(|x| x.write(data_u8));
}
});
}
@@ -2343,6 +2529,9 @@ impl AudioHandler {
device: &Device,
) -> ResultType<()> {
self.device_channel = config.channels;
#[cfg(target_os = "windows")]
let err_fn = self.playback_recovery.new_error_callback();
#[cfg(not(target_os = "windows"))]
let err_fn = move |err| {
// too many errors, will improve later
log::trace!("an error occurred on stream: {}", err);
@@ -2350,63 +2539,28 @@ impl AudioHandler {
self.audio_buffer
.resize(config.sample_rate.0 as _, config.channels as _);
let audio_buffer = self.audio_buffer.0.clone();
let ready = self.ready.clone();
let discontinuity_generation = self.audio_buffer.3.clone();
let mut playback_writer = audio_playback::AudioPlaybackWriter::new(
audio_playback::AudioPlaybackConfig {
sample_rate: config.sample_rate.0,
channels: config.channels as usize,
},
audio_buffer,
discontinuity_generation,
)?;
let playback_status = playback_writer.status.clone();
let timeout = None;
let stream = device.build_output_stream(
config,
move |data: &mut [T], info: &cpal::OutputCallbackInfo| {
if !*ready.lock().unwrap() {
*ready.lock().unwrap() = true;
}
let mut n = data.len();
let mut lock = audio_buffer.lock().unwrap();
let mut having = lock.occupied_len();
// android two timestamps, one from zero, another not
#[cfg(not(target_os = "android"))]
if having < n {
let tms = info.timestamp();
let how_long = tms
.playback
.duration_since(&tms.callback)
.unwrap_or(Duration::from_millis(0));
// must long enough to fight back scheuler delay
if how_long > Duration::from_millis(6) && how_long < Duration::from_millis(3000)
{
drop(lock);
std::thread::sleep(how_long.div_f32(1.2));
lock = audio_buffer.lock().unwrap();
having = lock.occupied_len();
}
if having < n {
n = having;
}
}
#[cfg(target_os = "android")]
if having < n {
n = having;
}
let mut elems = vec![0.0f32; n];
if n > 0 {
lock.pop_slice(&mut elems);
}
drop(lock);
let mut input = elems.into_iter();
for sample in data.iter_mut() {
*sample = match input.next() {
Some(x) => T::from_sample(x),
_ => T::from_sample(0.),
};
}
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
playback_writer.write_output(data);
},
err_fn,
timeout,
)?;
stream.play()?;
self.audio_stream = Some(Box::new(stream));
self.playback_status = playback_status;
Ok(())
}
}
@@ -2426,6 +2580,27 @@ mod audio_format_tests {
assert!(!is_supported_audio_channel_count(0));
assert!(!is_supported_audio_channel_count(u32::MAX));
}
#[test]
fn failed_audio_start_discards_format_state() {
use super::{anyhow, AudioDecoder, AudioHandler, Stereo};
const SAMPLE_RATE: u32 = 48_000;
const CHANNELS: u16 = 2;
let decoder = AudioDecoder::new(SAMPLE_RATE, Stereo).unwrap();
let mut handler = AudioHandler {
audio_decoder: Some((decoder, Vec::new())),
sample_rate: (SAMPLE_RATE, SAMPLE_RATE),
channels: CHANNELS,
..Default::default()
};
handler.handle_audio_start_result(Err(anyhow!("Injected playback startup failure")), false);
assert!(handler.audio_decoder.is_none());
assert_eq!(handler.channels, 0);
assert_eq!(handler.sample_rate, (0, 0));
}
}
/// Video handler for the [`Client`].
@@ -3929,7 +4104,11 @@ pub fn start_audio_thread() -> MediaSender {
std::thread::spawn(move || {
let mut audio_handler = AudioHandler::default();
loop {
if let Ok(data) = audio_receiver.recv() {
#[cfg(target_os = "windows")]
let received = audio_handler.receive_audio(&audio_receiver);
#[cfg(not(target_os = "windows"))]
let received = audio_receiver.recv();
if let Ok(data) = received {
match data {
MediaData::AudioFrame(af) => {
audio_handler.handle_frame(*af);

View File

@@ -0,0 +1,233 @@
use hbb_common::{log, log_throttle::LogThrottle, thiserror};
use ringbuf::{ring_buffer::RbBase, Rb};
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
TryLockError,
};
pub(super) const UNDERRUN_DECLICK_MS: usize = 5;
pub(super) const AUDIO_PLAYBACK_LOG_INTERVAL: std::time::Duration =
std::time::Duration::from_secs(5);
const MILLISECONDS_PER_SECOND: usize = 1_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct AudioPlaybackConfig {
pub sample_rate: u32,
pub channels: usize,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub(super) enum AudioPlaybackError {
#[error(
"invalid audio playback configuration: sample_rate={}, channels={}",
.0.sample_rate, .0.channels
)]
InvalidConfig(AudioPlaybackConfig),
#[error("audio playback frame has {samples} samples for {channels} channels")]
IncompleteFrame { samples: usize, channels: usize },
#[error("audio playback transition frame count overflow")]
FrameCountOverflow,
}
pub(super) struct AudioPlaybackRecovery {
channels: usize,
transition_frames: usize,
transition_frame: usize,
had_input: bool,
transition_start: Vec<f32>,
output_frame: Vec<f32>,
}
pub(super) struct AudioPlaybackStatus {
pub(super) ready: AtomicBool,
contentions: AtomicUsize,
contention_log_throttle: LogThrottle,
buffer_poisoned: AtomicBool,
}
impl Default for AudioPlaybackStatus {
fn default() -> Self {
Self {
ready: AtomicBool::new(false),
contentions: AtomicUsize::new(0),
contention_log_throttle: LogThrottle::new(AUDIO_PLAYBACK_LOG_INTERVAL),
buffer_poisoned: AtomicBool::new(false),
}
}
}
impl AudioPlaybackStatus {
pub(super) fn report_errors(&self) {
if self.contentions.load(Ordering::Relaxed) != 0
&& self.contention_log_throttle.due().is_some()
{
let contentions = self.contentions.swap(0, Ordering::Relaxed);
log::debug!("Audio playback PCM buffer contention: callbacks={contentions}");
}
if self.buffer_poisoned.swap(false, Ordering::Relaxed) {
log::error!("Audio playback stopped reading a poisoned PCM buffer");
}
}
}
pub(super) struct AudioPlaybackWriter {
audio_buffer: std::sync::Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>,
discontinuity_generation: std::sync::Arc<AtomicUsize>,
observed_discontinuity_generation: usize,
buffered_input: Vec<f32>,
recovery: AudioPlaybackRecovery,
pub(super) status: std::sync::Arc<AudioPlaybackStatus>,
buffer_failed: bool,
}
impl AudioPlaybackWriter {
pub(super) fn new(
config: AudioPlaybackConfig,
audio_buffer: std::sync::Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>,
discontinuity_generation: std::sync::Arc<AtomicUsize>,
) -> Result<Self, AudioPlaybackError> {
let recovery = AudioPlaybackRecovery::new(config)?;
let buffer_capacity = audio_buffer.lock().unwrap().capacity();
let observed_discontinuity_generation = discontinuity_generation.load(Ordering::Relaxed);
Ok(Self {
audio_buffer,
discontinuity_generation,
observed_discontinuity_generation,
buffered_input: vec![0.0; buffer_capacity],
recovery,
status: Default::default(),
buffer_failed: false,
})
}
fn read_buffer(&mut self, requested_samples: usize) -> usize {
if self.buffer_failed {
return 0;
}
let mut buffer = match self.audio_buffer.try_lock() {
Ok(buffer) => buffer,
Err(TryLockError::WouldBlock) => {
// Keep queued PCM and its generation for the next successful read.
self.status.contentions.fetch_add(1, Ordering::Relaxed);
return 0;
}
Err(TryLockError::Poisoned(_)) => {
self.buffer_failed = true;
self.status.ready.store(false, Ordering::Release);
self.status.buffer_poisoned.store(true, Ordering::Relaxed);
return 0;
}
};
let generation = self.discontinuity_generation.load(Ordering::Relaxed);
let channels = self.recovery.channels;
let samples = buffer.occupied_len().min(requested_samples) / channels * channels;
buffer.pop_slice(&mut self.buffered_input[..samples]);
drop(buffer);
if generation != self.observed_discontinuity_generation {
self.recovery.begin_discontinuity();
self.observed_discontinuity_generation = generation;
}
samples
}
pub(super) fn write_output<T>(&mut self, output: &mut [T])
where
T: cpal::Sample + cpal::FromSample<f32>,
{
self.status
.ready
.store(!self.buffer_failed, Ordering::Release);
let requested_samples = output.len().min(self.buffered_input.len());
let channel_count = self.recovery.channels;
let available_samples = self.read_buffer(requested_samples);
let available_frames = available_samples / channel_count;
for (frame_index, output_frame) in output.chunks_mut(channel_count).enumerate() {
let input = if frame_index < available_frames {
let start = frame_index * channel_count;
Some(&self.buffered_input[start..start + channel_count])
} else {
None
};
match self.recovery.process_frame(input) {
Ok(recovered) => {
for (output, sample) in output_frame.iter_mut().zip(recovered) {
*output = T::from_sample(*sample);
}
}
Err(error) => {
log::error!("Failed to recover audio underflow: {error}");
output_frame.fill(T::from_sample(0.0));
}
}
}
}
}
impl AudioPlaybackRecovery {
pub(super) fn new(config: AudioPlaybackConfig) -> Result<Self, AudioPlaybackError> {
if config.sample_rate == 0 || config.channels == 0 {
return Err(AudioPlaybackError::InvalidConfig(config));
}
let transition_frames = (config.sample_rate as usize)
.checked_mul(UNDERRUN_DECLICK_MS)
.ok_or(AudioPlaybackError::FrameCountOverflow)?
/ MILLISECONDS_PER_SECOND;
if transition_frames == 0 {
return Err(AudioPlaybackError::InvalidConfig(config));
}
Ok(Self {
channels: config.channels,
transition_frames,
transition_frame: transition_frames,
had_input: false,
transition_start: vec![0.0; config.channels],
output_frame: vec![0.0; config.channels],
})
}
pub(super) fn process_frame(
&mut self,
input: Option<&[f32]>,
) -> Result<&[f32], AudioPlaybackError> {
if input.is_some_and(|frame| frame.len() != self.channels) {
return Err(AudioPlaybackError::IncompleteFrame {
samples: input.map_or(0, <[f32]>::len),
channels: self.channels,
});
}
self.begin_transition(input.is_some());
let target_weight = self.advance_transition();
for channel in 0..self.channels {
let target = input.map_or(0.0, |frame| frame[channel]);
self.output_frame[channel] =
self.transition_start[channel] * (1.0 - target_weight) + target * target_weight;
}
Ok(&self.output_frame)
}
pub(super) fn begin_discontinuity(&mut self) {
self.transition_start.copy_from_slice(&self.output_frame);
self.transition_frame = 0;
}
fn begin_transition(&mut self, has_input: bool) {
if has_input == self.had_input {
return;
}
self.transition_start.copy_from_slice(&self.output_frame);
self.transition_frame = 0;
self.had_input = has_input;
}
fn advance_transition(&mut self) -> f32 {
if self.transition_frame >= self.transition_frames {
return 1.0;
}
self.transition_frame += 1;
self.transition_frame as f32 / self.transition_frames as f32
}
}
#[cfg(test)]
#[path = "audio_playback_tests.rs"]
mod tests;

View File

@@ -0,0 +1,184 @@
use super::{AudioDecoder, AudioFormat, AudioHandler, MediaData, Mono, Stereo};
use cpal::StreamError;
use crossbeam_queue::SegQueue;
use hbb_common::{log, tokio::time::Instant, ResultType};
use std::{
sync::{atomic::Ordering, mpsc, Arc},
time::Duration,
};
const RECOVERY_INTERVAL: Duration = Duration::from_secs(1);
pub(super) const STARTUP_CONFIRMATION_TIMEOUT: Duration = Duration::from_secs(5);
// The pinned WASAPI backend reports this warning but keeps its worker running.
const PRIORITY_WARNING_PREFIX: &str = "SetThreadPriority failed: ";
#[path = "audio_playback_startup.rs"]
mod startup;
#[derive(Default)]
pub(super) struct PlaybackRecovery {
pub(super) errors: Arc<SegQueue<StreamError>>,
format: Option<AudioFormat>,
pub(super) retry_at: Option<Instant>,
restart_not_before: Option<Instant>,
awaiting_callback: bool,
startup_deadline: Option<Instant>,
pending_output: Option<Box<AudioHandler>>,
}
impl PlaybackRecovery {
pub(super) fn new_error_callback(&mut self) -> impl FnMut(StreamError) + Send + 'static {
self.errors = Default::default();
let errors = self.errors.clone();
move |error| errors.push(error)
}
pub(super) fn report_pending(&self) -> bool {
let mut failed = false;
while let Some(error) = self.errors.pop() {
if matches!(&error, StreamError::BackendSpecific { err }
if err.description.starts_with(PRIORITY_WARNING_PREFIX))
{
log::warn!("Audio playback nonterminal priority warning: {error}");
} else {
log::error!("Audio playback stream failed: {error}");
failed = true;
}
}
failed
}
}
impl AudioHandler {
fn clear_playback_stream(&mut self) {
// Dropping CPAL may join its worker; run this on the owner, not its callback.
self.audio_stream = None;
self.playback_recovery.report_pending();
self.playback_status.report_errors();
let recovery = std::mem::take(self).playback_recovery;
self.playback_recovery.format = recovery.format;
self.playback_recovery.retry_at = recovery.retry_at;
self.playback_recovery.restart_not_before = recovery.restart_not_before;
}
pub(super) fn prepare_playback(&mut self, format: &AudioFormat) {
self.clear_playback_stream();
self.playback_recovery.format = Some(format.clone());
self.playback_recovery.retry_at = None;
self.playback_recovery.restart_not_before = None;
}
pub(super) fn finish_playback_start(&mut self, result: ResultType<()>) {
let now = Instant::now();
let retry_at = now + RECOVERY_INTERVAL;
self.playback_recovery.restart_not_before = Some(retry_at);
match result {
Ok(()) => {
self.playback_recovery.retry_at = None;
self.playback_recovery.awaiting_callback = true;
self.playback_recovery.startup_deadline = Some(now + STARTUP_CONFIRMATION_TIMEOUT);
log::info!("Audio playback stream opened; waiting for output callback");
}
Err(error) => {
self.clear_playback_stream();
self.playback_recovery.retry_at = Some(retry_at);
log::error!(
"Audio playback start failed: {error:#}; retrying in {RECOVERY_INTERVAL:?}"
);
}
}
}
fn playback_start_timed_out(&mut self, now: Instant) -> bool {
if !self.playback_recovery.awaiting_callback
|| self.playback_status.ready.load(Ordering::Acquire)
|| !self
.playback_recovery
.startup_deadline
.is_some_and(|due| now >= due)
{
return false;
}
self.playback_recovery.awaiting_callback = false;
self.playback_recovery.startup_deadline = None;
log::error!("Audio playback start timed out waiting for output callback");
true
}
fn restart_playback(&mut self, format: AudioFormat) -> ResultType<()> {
let channels = if format.channels > 1 { Stereo } else { Mono };
let decoder = AudioDecoder::new(format.sample_rate, channels)?;
let buffer = vec![0.; format.sample_rate as usize * format.channels as usize];
let channel_count = format.channels as _;
self.start_audio(format)?;
self.channels = channel_count;
self.audio_decoder = Some((decoder, buffer));
Ok(())
}
pub(super) fn recover_playback_with(
&mut self,
now: Instant,
restart: impl FnOnce(&mut Self, AudioFormat) -> ResultType<()>,
) {
let failed = self.resolve_pending_playback(now).unwrap_or_else(|| {
self.playback_recovery.report_pending() || self.playback_start_timed_out(now)
});
if failed {
self.clear_playback_stream();
self.playback_recovery.retry_at = Some(
self.playback_recovery
.restart_not_before
.map_or(now, |due| due.max(now)),
);
}
if self.playback_recovery.awaiting_callback
&& self.playback_status.ready.load(Ordering::Acquire)
{
self.playback_recovery.awaiting_callback = false;
self.playback_recovery.startup_deadline = None;
log::info!("Audio playback output callback started");
}
if !self
.playback_recovery
.retry_at
.is_some_and(|due| now >= due)
{
return;
}
let Some(format) = self.playback_recovery.format.clone() else {
return;
};
log::info!("Recreating audio playback on the current default output device");
let result = restart(self, format);
self.finish_playback_start(result);
}
pub(super) fn receive_audio(
&mut self,
receiver: &mpsc::Receiver<MediaData>,
) -> Result<MediaData, mpsc::RecvError> {
receive_with_recovery(receiver, RECOVERY_INTERVAL, || {
self.recover_playback_with(Instant::now(), Self::restart_playback);
})
}
}
pub(super) fn receive_with_recovery(
receiver: &mpsc::Receiver<MediaData>,
interval: Duration,
mut recover: impl FnMut(),
) -> Result<MediaData, mpsc::RecvError> {
loop {
match receiver.recv_timeout(interval) {
Ok(data) => {
if !matches!(data, MediaData::AudioFormat(_)) {
recover();
}
return Ok(data);
}
Err(mpsc::RecvTimeoutError::Timeout) => recover(),
Err(mpsc::RecvTimeoutError::Disconnected) => return Err(mpsc::RecvError),
}
}
}

View File

@@ -0,0 +1,63 @@
use super::{AudioHandler, Instant, Ordering, ResultType};
use hbb_common::log;
impl AudioHandler {
pub(in crate::client) fn cancel_pending_playback(&mut self) -> bool {
// Format messages bypass recovery; retain a usable candidate before superseding it.
let failed = self
.resolve_pending_playback(Instant::now())
.unwrap_or(false);
if let Some(mut pending) = self.playback_recovery.pending_output.take() {
pending.audio_stream = None;
pending.playback_recovery.report_pending();
pending.playback_status.report_errors();
}
failed
}
pub(in crate::client) fn finish_playback_replacement(
&mut self,
result: ResultType<()>,
previous: Option<Self>,
) {
self.finish_playback_start(result);
let Some(mut previous) = previous else {
return;
};
previous.audio_decoder = self.audio_decoder.take();
let candidate = std::mem::replace(self, previous);
self.playback_recovery.pending_output = Some(Box::new(candidate));
log::info!("Audio playback replacement pending; continuing on the compatible output");
self.recover_playback_with(Instant::now(), Self::restart_playback);
}
pub(super) fn resolve_pending_playback(&mut self, now: Instant) -> Option<bool> {
let mut candidate = self.playback_recovery.pending_output.take()?;
let candidate_failed =
candidate.playback_recovery.report_pending() || candidate.playback_start_timed_out(now);
let previous_failed =
self.playback_recovery.report_pending() || self.playback_start_timed_out(now);
if candidate_failed {
self.playback_recovery.restart_not_before =
candidate.playback_recovery.restart_not_before;
candidate.audio_stream = None;
candidate.playback_recovery.report_pending();
candidate.playback_status.report_errors();
if !previous_failed {
log::error!("Audio playback replacement failed before startup confirmation; keeping the existing compatible stream");
}
return Some(previous_failed);
}
if candidate.playback_status.ready.load(Ordering::Acquire) || previous_failed {
candidate.audio_decoder = self.audio_decoder.take();
self.audio_stream = None;
self.playback_recovery.report_pending();
self.playback_status.report_errors();
*self = *candidate;
return Some(false);
}
self.playback_recovery.pending_output = Some(candidate);
// A second active-queue read could discard a healthy pending candidate.
Some(false)
}
}

View File

@@ -0,0 +1,262 @@
use super::{
AudioPlaybackConfig, AudioPlaybackError, AudioPlaybackRecovery, AudioPlaybackStatus,
AudioPlaybackWriter,
};
use ringbuf::{ring_buffer::RbBase, Rb};
use std::{
sync::{atomic::Ordering, mpsc, Arc, Mutex},
time::Duration,
};
const SAMPLE_RATE: u32 = 48_000;
const CHANNELS: usize = 2;
const ACTIVE_FRAME: [f32; CHANNELS] = [0.8, -0.8];
const OPPOSITE_ACTIVE_FRAME: [f32; CHANNELS] = [-0.8, 0.8];
const ACTIVE_FRAMES: usize = 300;
const SILENT_FRAMES: usize = 300;
const TRANSITION_FRAMES: usize =
SAMPLE_RATE as usize * super::UNDERRUN_DECLICK_MS / super::MILLISECONDS_PER_SECOND;
const MAX_SAMPLE_STEP: f32 = 0.01;
#[test]
fn writing_audio_observes_discard_and_releases_buffer_lock() {
const INPUT: [f32; 4] = [0.1, 0.2, 0.3, 0.4];
const GENERATION: usize = 7;
let buffer = Arc::new(Mutex::new(ringbuf::HeapRb::new(INPUT.len())));
let generation = Arc::new(super::AtomicUsize::new(0));
let config = AudioPlaybackConfig {
sample_rate: SAMPLE_RATE,
channels: CHANNELS,
};
let mut writer = AudioPlaybackWriter::new(config, buffer.clone(), generation.clone()).unwrap();
{
let mut buffer = buffer.lock().unwrap();
buffer.push_slice(&INPUT);
generation.store(GENERATION, Ordering::Relaxed);
}
let mut output = [0.0_f32; INPUT.len()];
writer.write_output(&mut output);
assert_eq!(writer.buffered_input, INPUT);
assert_eq!(writer.observed_discontinuity_generation, GENERATION);
assert_eq!(buffer.try_lock().unwrap().occupied_len(), 0);
}
fn maximum_sample_step(samples: &[f32]) -> f32 {
samples
.windows(CHANNELS + 1)
.map(|window| (window[CHANNELS] - window[0]).abs())
.fold(0.0, f32::max)
}
#[test]
fn smooths_underflow_and_explicit_audio_discontinuities() {
for explicit_discontinuity in [false, true] {
let config = AudioPlaybackConfig {
sample_rate: SAMPLE_RATE,
channels: CHANNELS,
};
let mut recovery = AudioPlaybackRecovery::new(config).unwrap();
let mut output = Vec::new();
for _ in 0..ACTIVE_FRAMES {
output.extend_from_slice(recovery.process_frame(Some(&ACTIVE_FRAME)).unwrap());
}
let transition_end = TRANSITION_FRAMES * CHANNELS;
assert_eq!(
&output[transition_end - CHANNELS..transition_end],
ACTIVE_FRAME.as_slice(),
"explicit_discontinuity={explicit_discontinuity}"
);
let resumed_frame = if explicit_discontinuity {
recovery.begin_discontinuity();
&OPPOSITE_ACTIVE_FRAME
} else {
for _ in 0..SILENT_FRAMES {
output.extend_from_slice(recovery.process_frame(None).unwrap());
}
&ACTIVE_FRAME
};
for _ in 0..ACTIVE_FRAMES {
output.extend_from_slice(recovery.process_frame(Some(resumed_frame)).unwrap());
}
let maximum = maximum_sample_step(&output);
assert!(
maximum <= MAX_SAMPLE_STEP,
"step {maximum} exceeded {MAX_SAMPLE_STEP}, explicit={explicit_discontinuity}"
);
assert_eq!(
&output[output.len() - CHANNELS..],
resumed_frame,
"explicit_discontinuity={explicit_discontinuity}"
);
}
}
#[test]
fn validates_configuration_and_frame_size() {
let invalid_config = AudioPlaybackConfig {
sample_rate: 0,
channels: CHANNELS,
};
assert_eq!(
AudioPlaybackRecovery::new(invalid_config).err(),
Some(AudioPlaybackError::InvalidConfig(invalid_config))
);
let config = AudioPlaybackConfig {
sample_rate: SAMPLE_RATE,
channels: CHANNELS,
};
let mut recovery = AudioPlaybackRecovery::new(config).unwrap();
assert_eq!(
recovery.process_frame(Some(&[0.5])).err(),
Some(AudioPlaybackError::IncompleteFrame {
samples: 1,
channels: CHANNELS,
})
);
}
const CALLBACK_SAMPLES: usize = 64;
const CALLBACK_TIMEOUT: Duration = Duration::from_secs(2);
const DISCARD_GENERATION: usize = 1;
fn write_while_buffer_is_locked(
mut writer: AudioPlaybackWriter,
buffer: &Arc<Mutex<ringbuf::HeapRb<f32>>>,
generation: &Arc<super::AtomicUsize>,
) -> (AudioPlaybackWriter, [f32; CALLBACK_SAMPLES]) {
let mut guard = buffer.lock().unwrap();
let queued = OPPOSITE_ACTIVE_FRAME.repeat(ACTIVE_FRAMES);
guard.push_slice(&queued);
generation.store(DISCARD_GENERATION, Ordering::Relaxed);
let (completed_tx, completed_rx) = mpsc::channel();
let callback = std::thread::spawn(move || {
let mut output = [0.0; CALLBACK_SAMPLES];
crate::audio_resampler::allocation_tests::assert_no_allocations(|| {
writer.write_output(&mut output);
});
completed_tx.send((writer, output)).unwrap();
});
let completed = completed_rx.recv_timeout(CALLBACK_TIMEOUT);
let retained = guard.occupied_len();
drop(guard);
callback.join().unwrap();
let result = completed.expect("playback callback waited for the buffer owner");
assert_eq!(retained, queued.len());
result
}
#[test]
fn playback_contention_preserves_queued_audio_and_recovers_after_release() {
let samples = ACTIVE_FRAMES * CHANNELS;
let buffer = Arc::new(Mutex::new(ringbuf::HeapRb::new(samples)));
let generation = Arc::new(super::AtomicUsize::new(0));
let config = AudioPlaybackConfig {
sample_rate: SAMPLE_RATE,
channels: CHANNELS,
};
let mut writer = AudioPlaybackWriter::new(config, buffer.clone(), generation.clone()).unwrap();
buffer
.lock()
.unwrap()
.push_slice(&ACTIVE_FRAME.repeat(ACTIVE_FRAMES));
let mut output = vec![0.0; samples];
writer.write_output(&mut output);
assert_eq!(&output[samples - CHANNELS..], &ACTIVE_FRAME);
let (mut writer, gap) = write_while_buffer_is_locked(writer, &buffer, &generation);
assert_eq!(writer.status.contentions.load(Ordering::Relaxed), 1);
assert!(writer.status.ready.load(Ordering::Acquire));
assert_eq!(writer.observed_discontinuity_generation, 0);
assert!(maximum_sample_step(&gap) <= MAX_SAMPLE_STEP);
assert!(gap[0] > 0.0 && gap[0] < ACTIVE_FRAME[0]);
assert_eq!(gap[1], -gap[0]);
assert!(gap[CALLBACK_SAMPLES - CHANNELS] > 0.0);
writer.write_output(&mut output);
let mut transition = gap[gap.len() - CHANNELS..].to_vec();
transition.extend_from_slice(&output[..TRANSITION_FRAMES * CHANNELS]);
assert!(maximum_sample_step(&transition) <= MAX_SAMPLE_STEP);
assert_eq!(
writer.buffered_input,
OPPOSITE_ACTIVE_FRAME.repeat(ACTIVE_FRAMES)
);
assert_eq!(writer.observed_discontinuity_generation, DISCARD_GENERATION);
assert_eq!(&output[samples - CHANNELS..], &OPPOSITE_ACTIVE_FRAME);
assert_eq!(buffer.lock().unwrap().occupied_len(), 0);
}
#[test]
fn poisoned_playback_buffer_reports_once_without_panicking_in_the_callback() {
let buffer = Arc::new(Mutex::new(ringbuf::HeapRb::new(CALLBACK_SAMPLES)));
let config = AudioPlaybackConfig {
sample_rate: SAMPLE_RATE,
channels: CHANNELS,
};
let mut writer =
AudioPlaybackWriter::new(config, buffer.clone(), Arc::new(super::AtomicUsize::new(0)))
.unwrap();
assert!(std::thread::spawn(move || {
let _guard = buffer.lock().unwrap();
panic!("Injected PCM buffer failure");
})
.join()
.is_err());
let mut output = [ACTIVE_FRAME[0]; CALLBACK_SAMPLES];
crate::audio_resampler::allocation_tests::assert_no_allocations(|| {
writer.write_output(&mut output);
});
assert_eq!(output, [0.0; CALLBACK_SAMPLES]);
assert!(!writer.status.ready.load(Ordering::Acquire));
assert!(writer.status.buffer_poisoned.load(Ordering::Relaxed));
writer.status.report_errors();
writer.write_output(&mut output);
assert!(!writer.status.buffer_poisoned.load(Ordering::Relaxed));
assert_eq!(writer.status.contentions.load(Ordering::Relaxed), 0);
assert!(!writer.status.ready.load(Ordering::Acquire));
}
#[test]
fn contention_counts_accumulate_until_the_next_report() {
let status = AudioPlaybackStatus::default();
status.report_errors();
status.contentions.fetch_add(1, Ordering::Relaxed);
status.report_errors();
assert_eq!(status.contentions.load(Ordering::Relaxed), 0);
let mut total = 0;
for callbacks in [3, 7, 2] {
total += callbacks;
status.contentions.fetch_add(callbacks, Ordering::Relaxed);
status.report_errors();
assert_eq!(status.contentions.load(Ordering::Relaxed), total);
}
status.buffer_poisoned.store(true, Ordering::Relaxed);
status.report_errors();
assert!(!status.buffer_poisoned.load(Ordering::Relaxed));
assert_eq!(status.contentions.load(Ordering::Relaxed), total);
std::thread::sleep(super::AUDIO_PLAYBACK_LOG_INTERVAL);
status.report_errors();
assert_eq!(status.contentions.load(Ordering::Relaxed), 0);
}
#[test]
fn contention_reporting_is_independent_between_playbacks() {
let first = AudioPlaybackStatus::default();
let second = AudioPlaybackStatus::default();
for status in [&first, &second] {
status.contentions.fetch_add(1, Ordering::Relaxed);
status.report_errors();
assert_eq!(status.contentions.load(Ordering::Relaxed), 0);
}
for status in [&first, &second] {
status.contentions.fetch_add(1, Ordering::Relaxed);
status.report_errors();
assert_eq!(status.contentions.load(Ordering::Relaxed), 1);
}
}

View File

@@ -15,6 +15,16 @@ use crate::{
// 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 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")]
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
use base::{
@@ -247,6 +257,9 @@ impl<T: InvokeUiSession> Remote<T> {
let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await;
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 {
tokio::select! {
@@ -313,6 +326,37 @@ impl<T: InvokeUiSession> Remote<T> {
self.handler.msgbox("restarting-show", "Restarting remote device", "Connection in progress. Please wait.", "");
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();
if elapsed < 1000 {
continue;
@@ -358,6 +402,11 @@ impl<T: InvokeUiSession> Remote<T> {
s.send(()).ok();
}
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.
self.send_close_reason(&mut peer, "kcp").await;
// KCP does not send messages immediately, so wait to ensure the last message is sent.
@@ -549,6 +598,9 @@ impl<T: InvokeUiSession> Remote<T> {
} else {
log::debug!("Failed to record local audio channel: {}", err);
}
// Both arms fall through with nothing else in this loop blocking, so
// without a pause the thread spun a core for the whole voice call.
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
}

View File

@@ -59,11 +59,14 @@ impl Screenshot {
}
fn handle_screenshot(&mut self, action: String) -> String {
let Some(data) = self.data.take() else {
let Some(data) = self.data.as_ref().cloned() else {
return "No cached screenshot".to_owned();
};
match Self::handle_screenshot_(data, action) {
Ok(()) => "".to_owned(),
Ok(()) => {
self.data = None;
"".to_owned()
}
Err(e) => e.to_string(),
}
}
@@ -98,3 +101,37 @@ pub fn set_screenshot(data: bytes::Bytes) {
pub fn handle_screenshot(action: String) -> String {
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

@@ -0,0 +1,226 @@
use super::*;
use crate::client::{
audio_playback::AudioPlaybackStatus, audio_playback_recovery::STARTUP_CONFIRMATION_TIMEOUT,
};
use cpal::StreamError;
use crossbeam_queue::SegQueue;
use hbb_common::tokio::time::Instant;
use std::time::Duration;
const AFTER_COOLDOWN: Duration = Duration::from_secs(2);
type PendingOutput = (
Arc<AtomicBool>,
Arc<AudioPlaybackStatus>,
Arc<SegQueue<StreamError>>,
);
fn install_output(handler: &mut AudioHandler, dropped: Arc<AtomicBool>) {
handler.sample_rate = (INPUT_RATE, OUTPUT_RATE);
handler.device_channel = CHANNELS;
handler.audio_stream = Some(Box::new(TrackedAudioStream(dropped)));
handler.playback_status.ready.store(true, Ordering::Release);
}
fn recovery_handler() -> (AudioHandler, Arc<AtomicBool>) {
let dropped = Arc::new(AtomicBool::new(false));
let mut handler = AudioHandler::default();
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
install_output(candidate, dropped.clone());
Ok(())
});
(handler, dropped)
}
fn begin_pending(handler: &mut AudioHandler) -> PendingOutput {
let dropped = Arc::new(AtomicBool::new(false));
let mut state = None;
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
install_output(candidate, dropped.clone());
candidate
.playback_status
.ready
.store(false, Ordering::Release);
state = Some((
candidate.playback_status.clone(),
candidate.playback_recovery.errors.clone(),
));
Ok(())
});
let (status, errors) = state.unwrap();
(dropped, status, errors)
}
#[test]
fn unconfirmed_start_retries_without_callback_or_error() {
let dropped = Arc::new(AtomicBool::new(false));
let mut handler = AudioHandler::default();
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
install_output(candidate, dropped.clone());
candidate
.playback_status
.ready
.store(false, Ordering::Release);
Ok(())
});
handler.recover_playback_with(Instant::now(), |_, _| {
panic!("Startup confirmation deadline has not elapsed")
});
assert!(!dropped.load(Ordering::SeqCst));
let expired = Instant::now() + STARTUP_CONFIRMATION_TIMEOUT;
let mut attempts = 0;
handler.recover_playback_with(expired, |candidate, requested| {
attempts += 1;
assert!(dropped.load(Ordering::SeqCst));
assert_eq!(requested, format(INPUT_RATE, CHANNELS));
install_output(candidate, Arc::new(AtomicBool::new(false)));
Ok(())
});
assert_eq!(attempts, 1);
handler.recover_playback_with(expired + STARTUP_CONFIRMATION_TIMEOUT, |_, _| {
panic!("Confirmed output must not be reopened")
});
}
#[test]
fn already_terminal_candidate_cannot_replace_compatible_output() {
let (mut handler, dropped) = recovery_handler();
let candidate_dropped = Arc::new(AtomicBool::new(false));
let old_buffer = handler.audio_buffer.0.clone();
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
install_output(candidate, candidate_dropped.clone());
candidate
.playback_recovery
.errors
.push(StreamError::DeviceNotAvailable);
Ok(())
});
assert!(!dropped.load(Ordering::SeqCst));
assert!(candidate_dropped.load(Ordering::SeqCst));
assert!(Arc::ptr_eq(&old_buffer, &handler.audio_buffer.0));
handler.recover_playback_with(Instant::now() + AFTER_COOLDOWN, |_, _| {
panic!("Compatible active output must not be reopened")
});
}
#[test]
fn pending_output_keeps_playing_and_transfers_decoder_history_on_commit() {
let (mut handler, old_dropped) = recovery_handler();
let old_buffer = handler.audio_buffer.0.clone();
let (_, status, _) = begin_pending(&mut handler);
let frame = audio_frame();
let (mut reference, mut expected) = decoder(INPUT_RATE);
reference
.decode_float(&frame.data, &mut expected, false)
.unwrap();
handler.handle_frame(frame.clone());
assert!(Arc::ptr_eq(&old_buffer, &handler.audio_buffer.0));
assert!(!drain_audio(&handler).is_empty());
status.ready.store(true, Ordering::Release);
handler.recover_playback_with(Instant::now(), |_, _| panic!("Candidate already exists"));
assert!(old_dropped.load(Ordering::SeqCst));
assert!(Arc::ptr_eq(&status, &handler.playback_status));
let samples = reference
.decode_float(&frame.data, &mut expected, false)
.unwrap()
* CHANNELS as usize;
handler.handle_frame(frame);
assert_eq!(
&handler.audio_decoder.as_ref().unwrap().1[..samples],
&expected[..samples]
);
}
#[test]
fn rollback_keeps_the_restarted_decoders_accumulated_history() {
let (mut handler, old_dropped) = recovery_handler();
let frame = audio_frame();
handler.handle_frame(frame.clone());
let (candidate_dropped, _, errors) = begin_pending(&mut handler);
let (mut reference, mut expected) = decoder(INPUT_RATE);
handler.handle_frame(frame.clone());
reference
.decode_float(&frame.data, &mut expected, false)
.unwrap();
errors.push(StreamError::DeviceNotAvailable);
handler.recover_playback_with(Instant::now(), |_, _| panic!("Old output still works"));
let samples = reference
.decode_float(&frame.data, &mut expected, false)
.unwrap()
* CHANNELS as usize;
handler.handle_frame(frame);
assert!(!old_dropped.load(Ordering::SeqCst));
assert!(candidate_dropped.load(Ordering::SeqCst));
assert_eq!(
&handler.audio_decoder.as_ref().unwrap().1[..samples],
&expected[..samples]
);
}
#[test]
fn later_compatible_format_retires_only_the_pending_attempt() {
let (mut handler, old_dropped) = recovery_handler();
let (first_dropped, _, first_errors) = begin_pending(&mut handler);
let (second_dropped, second_status, _) = begin_pending(&mut handler);
assert!(first_dropped.load(Ordering::SeqCst));
assert!(!old_dropped.load(Ordering::SeqCst));
first_errors.push(StreamError::DeviceNotAvailable);
second_status.ready.store(true, Ordering::Release);
handler.recover_playback_with(Instant::now(), |_, _| {
panic!("Retired attempt affected current output")
});
assert!(old_dropped.load(Ordering::SeqCst));
assert!(!second_dropped.load(Ordering::SeqCst));
assert!(Arc::ptr_eq(&second_status, &handler.playback_status));
}
#[test]
fn both_outputs_failing_retains_format_and_paces_recovery() {
let (mut handler, old_dropped) = recovery_handler();
let old_errors = handler.playback_recovery.errors.clone();
let (candidate_dropped, _, errors) = begin_pending(&mut handler);
old_errors.push(StreamError::DeviceNotAvailable);
errors.push(StreamError::DeviceNotAvailable);
handler.recover_playback_with(Instant::now(), |_, _| panic!("Retry must be paced"));
assert!(old_dropped.load(Ordering::SeqCst));
assert!(candidate_dropped.load(Ordering::SeqCst));
assert!(handler.audio_stream.is_none());
let due = handler.playback_recovery.retry_at.unwrap();
let mut attempts = 0;
handler.recover_playback_with(due, |_, requested| {
attempts += 1;
assert_eq!(requested, format(INPUT_RATE, CHANNELS));
Ok(())
});
assert_eq!(attempts, 1);
}
#[test]
fn superseding_format_keeps_ready_candidate_when_active_output_failed() {
let (mut handler, old_dropped) = recovery_handler();
let old_errors = handler.playback_recovery.errors.clone();
let (candidate_dropped, candidate_status, _) = begin_pending(&mut handler);
candidate_status.ready.store(true, Ordering::Release);
old_errors.push(StreamError::DeviceNotAvailable);
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), failed_output);
assert!(!candidate_dropped.load(Ordering::SeqCst));
assert!(old_dropped.load(Ordering::SeqCst));
assert!(Arc::ptr_eq(&candidate_status, &handler.playback_status));
assert!(handler.audio_decoder.is_some());
assert!(handler.playback_recovery.retry_at.is_none());
}
#[test]
fn superseding_format_preserves_failure_when_both_outputs_failed() {
let (mut handler, old_dropped) = recovery_handler();
let old_errors = handler.playback_recovery.errors.clone();
let (candidate_dropped, status, errors) = begin_pending(&mut handler);
status.ready.store(true, Ordering::Release);
old_errors.push(StreamError::DeviceNotAvailable);
errors.push(StreamError::DeviceNotAvailable);
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), failed_output);
assert!(candidate_dropped.load(Ordering::SeqCst));
assert!(old_dropped.load(Ordering::SeqCst));
assert!(handler.audio_stream.is_none());
assert!(handler.playback_recovery.retry_at.is_some());
}

View File

@@ -0,0 +1,128 @@
use super::{create_audio_resampler, AudioDecoder, AudioFormat, AudioFrame, AudioHandler, Stereo};
use cpal::traits::StreamTrait;
use hbb_common::{anyhow::anyhow, ResultType};
use magnum_opus::{Application::LowDelay, Encoder};
use ringbuf::Rb;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
const INPUT_RATE: u32 = 24_000;
const OUTPUT_RATE: u32 = 48_000;
const CHANNELS: u16 = 2;
const PACKETS_PER_SECOND: usize = 100;
const MAX_PACKET_BYTES: usize = 4_096;
const SAMPLE_VALUE: f32 = 0.25;
const MONO_CHANNELS: u16 = 1;
#[cfg(target_os = "windows")]
#[path = "audio_playback_recovery_tests.rs"]
mod recovery_tests;
struct TrackedAudioStream(Arc<AtomicBool>);
impl StreamTrait for TrackedAudioStream {
fn play(&self) -> Result<(), cpal::PlayStreamError> {
Ok(())
}
fn pause(&self) -> Result<(), cpal::PauseStreamError> {
Ok(())
}
}
impl Drop for TrackedAudioStream {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
fn decoder(sample_rate: u32) -> (AudioDecoder, Vec<f32>) {
(
AudioDecoder::new(sample_rate, Stereo).unwrap(),
vec![0.0; sample_rate as usize * CHANNELS as usize],
)
}
fn active_handler(input_rate: u32) -> (AudioHandler, Arc<AtomicBool>) {
let dropped = Arc::new(AtomicBool::new(false));
let handler = AudioHandler {
audio_decoder: Some(decoder(input_rate)),
audio_resampler: create_audio_resampler(input_rate, OUTPUT_RATE, CHANNELS).unwrap(),
sample_rate: (input_rate, OUTPUT_RATE),
audio_stream: Some(Box::new(TrackedAudioStream(dropped.clone()))),
channels: CHANNELS,
device_channel: CHANNELS,
..Default::default()
};
handler.playback_status.ready.store(true, Ordering::Release);
(handler, dropped)
}
fn audio_frame() -> AudioFrame {
let samples = OUTPUT_RATE as usize / PACKETS_PER_SECOND * CHANNELS as usize;
let mut encoder = Encoder::new(OUTPUT_RATE, Stereo, LowDelay).unwrap();
AudioFrame {
data: encoder
.encode_vec_float(&vec![SAMPLE_VALUE; samples], MAX_PACKET_BYTES)
.unwrap()
.into(),
..Default::default()
}
}
fn failed_output(candidate: &mut AudioHandler, _: AudioFormat) -> ResultType<()> {
candidate.sample_rate = (INPUT_RATE, INPUT_RATE);
candidate.device_channel = MONO_CHANNELS;
candidate
.audio_buffer
.resize(INPUT_RATE as _, MONO_CHANNELS as _);
candidate
.playback_status
.ready
.store(false, Ordering::Release);
Err(anyhow!("Injected playback failure"))
}
fn format(sample_rate: u32, channels: u16) -> AudioFormat {
AudioFormat {
sample_rate,
channels: u32::from(channels),
..Default::default()
}
}
#[test]
fn identical_format_failure_preserves_playback_and_resampler_history() {
let (mut handler, dropped) = active_handler(INPUT_RATE);
let (mut reference, _) = active_handler(INPUT_RATE);
let (mut retained_decoder, _) = active_handler(INPUT_RATE);
retained_decoder.handle_frame(audio_frame());
retained_decoder.handle_frame(audio_frame());
handler.handle_frame(audio_frame());
reference.handle_frame(audio_frame());
let buffer = handler.audio_buffer.0.clone();
let status = handler.playback_status.clone();
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), failed_output);
reference.audio_decoder = Some(decoder(INPUT_RATE));
handler.handle_frame(audio_frame());
reference.handle_frame(audio_frame());
assert!(!dropped.load(Ordering::SeqCst));
assert!(Arc::ptr_eq(&buffer, &handler.audio_buffer.0));
assert!(Arc::ptr_eq(&status, &handler.playback_status));
assert_eq!(handler.sample_rate, (INPUT_RATE, OUTPUT_RATE));
assert_eq!(handler.device_channel, CHANNELS);
assert!(handler.playback_status.ready.load(Ordering::Acquire));
let expected = drain_audio(&reference);
let actual = drain_audio(&handler);
assert!(!actual.is_empty());
assert_ne!(drain_audio(&retained_decoder), expected);
assert_eq!(actual, expected);
}
fn drain_audio(handler: &AudioHandler) -> Vec<f32> {
handler.audio_buffer.0.lock().unwrap().pop_iter().collect()
}

View File

@@ -408,6 +408,11 @@ pub fn resample_channels(
}
}
#[cfg(all(feature = "use_dasp", feature = "use_samplerate"))]
compile_error!(
"features `use_dasp` and `use_samplerate` are mutually exclusive; disable default features before selecting `use_samplerate`"
);
#[cfg(feature = "use_dasp")]
pub fn audio_resample(
data: &[f32],
@@ -444,7 +449,7 @@ pub fn audio_resample(
}
}
#[cfg(feature = "use_samplerate")]
#[cfg(all(feature = "use_samplerate", not(feature = "use_dasp")))]
pub fn audio_resample(
data: &[f32],
sample_rate0: u32,

View File

@@ -1,6 +1,9 @@
use hbb_common::{
bail,
base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _},
base64::{
engine::general_purpose::{URL_SAFE, URL_SAFE_NO_PAD},
Engine as _,
},
sodiumoxide::crypto::sign,
ResultType,
};
@@ -25,7 +28,9 @@ fn get_custom_server_from_config_string(s: &str) -> ResultType<CustomServer> {
12, 46, 129, 83, 17, 84, 193, 119, 197, 130, 103,
];
let pk = sign::PublicKey(*PK);
let data = URL_SAFE_NO_PAD.decode(tmp)?;
let data = URL_SAFE_NO_PAD
.decode(&tmp)
.or_else(|_| URL_SAFE.decode(&tmp))?;
if let Ok(lic) = serde_json::from_slice::<CustomServer>(&data) {
return Ok(lic);
}
@@ -215,5 +220,16 @@ mod test {
assert_eq!(
get_custom_server_from_string("rustdesk-licensed--0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye--.exe")
.unwrap(), lic);
// padded base64 (one '=' after reversal) is accepted, wrong padding is not
assert_eq!(
get_custom_server_from_string("rustdesk-licensed-=0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye.exe")
.unwrap(), lic);
assert!(
get_custom_server_from_string("rustdesk-licensed-==0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye.exe")
.is_err());
// bare string as passed to `--config`
assert_eq!(
get_custom_server_from_string("=0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye.exe")
.unwrap(), lic);
}
}

View File

@@ -1497,7 +1497,13 @@ pub async fn start_pa() {
None, // Use default buffering attributes
) {
Ok(s) => loop {
if let Ok(_) = s.read(&mut buf) {
// A dead pulse handle fails every read at once, so ignoring the
// error left nothing pacing this loop and it burned a core.
if let Err(err) = s.read(&mut buf) {
log::error!("Failed to read audio data:{}", err);
break;
}
{
let out =
if buf.iter().filter(|x| **x != 0).next().is_none() {
vec![]

View File

@@ -8,14 +8,15 @@ use hbb_common::{
tokio_util, ResultType, Stream,
};
use kcp_sys::{
endpoint::KcpEndpoint,
endpoint::{ConnId, KcpEndpoint},
packet_def::{KcpPacket, KcpPacketHeader},
stream,
};
use std::{net::SocketAddr, sync::Arc};
pub struct KcpStream {
_endpoint: KcpEndpoint,
endpoint: KcpEndpoint,
conn_id: ConnId,
stop_sender: Option<oneshot::Sender<()>>,
}
@@ -41,6 +42,14 @@ 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 {
Stream::Tcp(FramedStream(
tokio_util::codec::Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
@@ -77,7 +86,8 @@ impl KcpStream {
if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) {
Ok((
Self {
_endpoint: endpoint,
endpoint,
conn_id,
stop_sender: Some(stop_sender),
},
Self::create_framed(stream, udp_socket.local_addr().ok()),
@@ -108,7 +118,8 @@ impl KcpStream {
if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) {
Ok((
Self {
_endpoint: endpoint,
endpoint,
conn_id,
stop_sender: Some(stop_sender),
},
Self::create_framed(stream, udp_socket.local_addr().ok()),

View File

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

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "تمكين اتصال نظير إلى نظير عبر WebRTC"),
("Enable TCP hole punching", "تمكين تقنية حفر الثغرات عبر TCP"),
("The screen sharing request was declined on the remote device", "تم رفض طلب مشاركة الشاشة على الجهاز البعيد"),
("No one responded to the screen sharing request on the remote device", "لم يستجب أحد لطلب مشاركة الشاشة على الجهاز البعيد"),
("The XDG Desktop Portal ended the screen sharing request ({})", "أنهى XDG Desktop Portal طلب مشاركة الشاشة ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "لم يُرجع XDG Desktop Portal أي شاشة لالتقاطها، قد تكون مكتبة PipeWire قديمة جدًا"),
("A GStreamer plugin needed for screen capture is missing ({})", "مكوّن GStreamer الإضافي اللازم لالتقاط الشاشة مفقود ({})"),
("The screen sharing request timed out on the remote device", "انتهت مهلة طلب مشاركة الشاشة على الجهاز البعيد"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "يتعذّر على RustDesk الوصول إلى جلسة سطح المكتب على الجهاز البعيد، تأكد من أن جلسة سطح المكتب تعمل وأن RustDesk يمكنه استخدامها"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "بوابة سطح المكتب على الجهاز البعيد تفتقر إلى إمكانية لازمة لمشاركة الشاشة أو التحكم عن بُعد، قد لا تكون واجهتها الخلفية مثبتة"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "تمت الموافقة على مشاركة الشاشة على الجهاز البعيد، لكن تعذّر فتح اتصال PipeWire"),
("The screen sharing request ended without completing on the remote device", "انتهى طلب مشاركة الشاشة على الجهاز البعيد دون أن يكتمل"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "تعذّر على RustDesk الحصول على شاشة قابلة للاستخدام من XDG Desktop Portal، قد تكون مكتبة PipeWire قديمة جدًا"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "تعذّر على RustDesk تحميل مكوّن GStreamer اللازم لالتقاط الشاشة ({})"),
("Relay fallback delay in seconds", "مهلة التراجع إلى الترحيل بالثواني"),
("relay-fallback-delay-tip", "المدة التي ينتظرها اتصال الترحيل القائم بالفعل الاتصالَ المباشر عبر WebRTC قبل أن يُستخدم بدلًا منه. زِدها لمنح الاتصال المباشر البطيء فرصة أكبر للفوز؛ وقلّلها للاستقرار على الترحيل أسرع في الشبكات التي يتعذر فيها الاتصال المباشر. اتركها فارغة للقيمة الافتراضية 2.5 ثانية."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

785
src/lang/az.rs Normal file
View File

@@ -0,0 +1,785 @@
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"),
("The screen sharing request was declined on the remote device", ""),
("The screen sharing request timed out on the remote device", ""),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", ""),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", ""),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", ""),
("The screen sharing request ended without completing on the remote device", ""),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", ""),
("RustDesk could not load a GStreamer component needed for screen capture ({})", ""),
("Relay fallback delay in seconds", "Ötürücüyə keçid gecikməsi, saniyə"),
("relay-fallback-delay-tip", "Artıq qurulmuş ötürücü bağlantı birbaşa WebRTC bağlantısını nə qədər gözləyir, sonra onun əvəzinə istifadə olunur. Yavaş birbaşa bağlantıya daha çox vaxt vermək üçün artırın; birbaşa bağlantının mümkün olmadığı şəbəkələrdə ötürücüyə daha tez keçmək üçün azaldın. Standart 2.5 saniyə üçün boş buraxın."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Выкарыстоўваць падключэнне WebRTC P2P"),
("Enable TCP hole punching", "Выкарыстоўваць TCP hole punching"),
("The screen sharing request was declined on the remote device", "Запыт на абагульванне экрана быў адхілены на аддаленай прыладзе"),
("No one responded to the screen sharing request on the remote device", "Ніхто не адказаў на запыт абагульвання экрана на аддаленай прыладзе"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal завяршыў запыт на абагульванне экрана ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal не вярнуў экран для захопу, магчыма бібліятэка PipeWire занадта старая"),
("A GStreamer plugin needed for screen capture is missing ({})", "Адсутнічае плагін GStreamer, патрэбны для захопу экрана ({})"),
("The screen sharing request timed out on the remote device", "Час чакання запыту на абагульванне экрана на аддаленай прыладзе выйшаў"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk не можа атрымаць доступ да сеанса працоўнага стала на аддаленай прыладзе, праверце, ці запушчаны сеанс і ці даступны ён для RustDesk"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Партал працоўнага стала на аддаленай прыладзе не мае магчымасці, патрэбнай для абагульвання экрана або аддаленага кіравання, магчыма не ўсталяваны яго бэкенд"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Абагульванне экрана было дазволена на аддаленай прыладзе, але не ўдалося адкрыць злучэнне PipeWire"),
("The screen sharing request ended without completing on the remote device", "Запыт на абагульванне экрана на аддаленай прыладзе завяршыўся, не будучы выкананым"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk не змог атрымаць прыдатны экран ад XDG Desktop Portal, магчыма бібліятэка PipeWire занадта старая"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не змог загрузіць кампанент GStreamer, патрэбны для захопу экрана ({})"),
("Relay fallback delay in seconds", "Затрымка пераходу на рэтранслятар у секундах"),
("relay-fallback-delay-tip", "Колькі часу ўжо ўсталяванае злучэнне праз рэтранслятар чакае прамога злучэння WebRTC, перш чым будзе выкарыстана замест яго. Павялічце, каб даць павольнаму прамому злучэнню больш часу; паменшыце, каб хутчэй пераходзіць на рэтранслятар у сетках, дзе прамое злучэнне немагчымае. Пакіньце пустым для значэння па змаўчанні 2.5 секунды."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Позволяване на WebRTC P2P връзка"),
("Enable TCP hole punching", "Позволяване на TCP hole punching"),
("The screen sharing request was declined on the remote device", "Заявката за споделяне на екрана беше отхвърлена на отдалеченото устройство"),
("No one responded to the screen sharing request on the remote device", "Никой не отговори на заявката за споделяне на екрана на отдалеченото устройство"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal прекрати заявката за споделяне на екрана ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal не върна екран за заснемане, библиотеката PipeWire може да е твърде стара"),
("A GStreamer plugin needed for screen capture is missing ({})", "Липсва приставка на GStreamer, необходима за заснемане на екрана ({})"),
("The screen sharing request timed out on the remote device", "Заявката за споделяне на екрана на отдалеченото устройство изтече"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk не може да достигне сесията на работния плот на отдалеченото устройство, проверете дали сесията работи и дали RustDesk може да я използва"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Порталът на работния плот на отдалеченото устройство няма възможност, необходима за споделяне на екрана или отдалечено управление, може да липсва неговата реализация"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Споделянето на екрана беше одобрено на отдалеченото устройство, но връзката с PipeWire не можа да бъде отворена"),
("The screen sharing request ended without completing on the remote device", "Заявката за споделяне на екрана на отдалеченото устройство приключи, без да бъде изпълнена"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk не можа да получи използваем екран от XDG Desktop Portal, библиотеката PipeWire може да е твърде стара"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не можа да зареди компонент на GStreamer, необходим за заснемане на екрана ({})"),
("Relay fallback delay in seconds", "Забавяне преди преминаване към препредаване в секунди"),
("relay-fallback-delay-tip", "Колко време вече установената връзка чрез препредаване изчаква директната WebRTC връзка, преди да бъде използвана вместо нея. Увеличете, за да дадете повече време на бавна директна връзка; намалете, за да се премине по-бързо към препредаване в мрежи, където директна връзка е невъзможна. Оставете празно за стойността по подразбиране 2.5 секунди."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Habilita la connexió WebRTC P2P"),
("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"),
("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"),
("The XDG Desktop Portal ended the screen sharing request ({})", "L'XDG Desktop Portal ha finalitzat la sol·licitud de compartició de pantalla ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Falta un connector del GStreamer necessari per capturar la pantalla ({})"),
("The screen sharing request timed out on the remote device", "La sol·licitud de compartició de pantalla ha esgotat el temps 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "La sol·licitud de compartició de pantalla al dispositiu remot ha acabat sense completar-se"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "El RustDesk no ha pogut obtenir cap pantalla utilitzable de l'XDG Desktop Portal; la biblioteca PipeWire pot ser massa antiga"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "El RustDesk no ha pogut carregar un component del GStreamer necessari per capturar la pantalla ({})"),
("Relay fallback delay in seconds", "Retard abans de recórrer al relé en segons"),
("relay-fallback-delay-tip", "Quant de temps espera una connexió de relé ja establerta la connexió directa WebRTC abans d'utilitzar-se en lloc seu. Augmenteu-lo per donar més temps a una connexió directa lenta; reduïu-lo per passar abans al relé en xarxes on no es pot fer una connexió directa. Deixeu-lo buit per al valor predeterminat de 2.5 segons."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -770,10 +770,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("port-forward-mux-tip", "同一条端口转发规则上的所有连接共用一条到对方的连接,而不是每条连接都重新连接并登录一次。"),
("Enable WebRTC P2P connection", "启用 WebRTC P2P 连接"),
("Enable TCP hole punching", "启用 TCP 打洞"),
("The screen sharing request was declined on the remote device", "远程设备上拒绝了屏幕共享请求"),
("No one responded to the screen sharing request on the remote device", "远程设备上无人响应屏幕共享请求"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal 结束了屏幕共享请求 ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal 未返回可捕获的屏幕PipeWire 库可能过旧"),
("A GStreamer plugin needed for screen capture is missing ({})", "缺少屏幕捕获所需的 GStreamer 插件 ({})"),
("The screen sharing request was declined on the remote device", "远程设备上的用户拒绝了屏幕共享请求"),
("The screen sharing request timed out on the remote device", "远程设备上屏幕共享请求超时了"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk 无法访问远程设备的桌面会话,请确认桌面会话已启动并且 RustDesk 可以使用它"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "远程设备上的桌面门户缺少屏幕共享或远程控制所需的功能,可能没有安装它的后端"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "远程设备上已批准屏幕共享,但无法打开 PipeWire 连接"),
("The screen sharing request ended without completing on the remote device", "远程设备上的屏幕共享请求已结束,但未完成"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk 无法从 XDG Desktop Portal 获取可用的屏幕PipeWire 库可能过旧"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk 无法加载屏幕捕获所需的 GStreamer 组件 ({})"),
("Relay fallback delay in seconds", "回落到中继前的等待时间(秒)"),
("relay-fallback-delay-tip", "已经建立的中继连接会等待直连的 WebRTC 多久,超过这个时间就改用中继。调大可以让较慢的直连有更多机会胜出;调小则在无法直连的网络上更快回落到中继。留空表示使用默认值 2.5 秒。"),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Povolit připojení WebRTC P2P"),
("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"),
("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"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal ukončil žádost o sdílení obrazovky ({})"),
("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á"),
("A GStreamer plugin needed for screen capture is missing ({})", "Chybí zásuvný modul GStreamer potřebný k zachycení obrazovky ({})"),
("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í"),
("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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Žádost o sdílení obrazovky na vzdáleném zařízení skončila, aniž by byla dokončena"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nezískal z XDG Desktop Portal použitelnou obrazovku, knihovna PipeWire může být příliš stará"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nemohl načíst komponentu GStreameru potřebnou k zachycení obrazovky ({})"),
("Relay fallback delay in seconds", "Prodleva před přepnutím na přenos v sekundách"),
("relay-fallback-delay-tip", "Jak dlouho již navázané spojení přes přenos čeká na přímé spojení WebRTC, než bude použito místo něj. Zvyšte, aby pomalé přímé spojení mělo více času uspět; snižte, aby se v sítích, kde přímé spojení není možné, dříve přešlo na přenos. Ponechte prázdné pro výchozí hodnotu 2.5 sekundy."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Aktivér WebRTC P2P-forbindelse"),
("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"),
("No one responded to the screen sharing request on the remote device", "Ingen svarede på anmodningen om skærmdeling på fjernenheden"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal afsluttede anmodningen om skærmdeling ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Et GStreamer-plugin, der kræves til skærmoptagelse, mangler ({})"),
("The screen sharing request timed out on the remote device", "Anmodningen om skærmdeling fik timeout 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Anmodningen om skærmdeling på fjernenheden sluttede uden at blive gennemført"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk kunne ikke få en brugbar skærm fra XDG Desktop Portal, PipeWire-biblioteket er måske for gammelt"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kunne ikke indlæse en GStreamer-komponent, der kræves til skærmoptagelse ({})"),
("Relay fallback delay in seconds", "Forsinkelse før brug af relæ i sekunder"),
("relay-fallback-delay-tip", "Hvor længe en allerede oprettet relæforbindelse venter på den direkte WebRTC-forbindelse, før den bruges i stedet. Forøg for at give en langsom direkte forbindelse mere tid; sænk for hurtigere at falde tilbage til relæet på netværk, hvor en direkte forbindelse ikke kan oprettes. Lad feltet stå tomt for standardværdien 2.5 sekunder."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC-P2P-Verbindung 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"),
("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"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal hat die Anfrage zur Bildschirmfreigabe beendet ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Ein für die Bildschirmaufnahme benötigtes GStreamer-Plugin fehlt ({})"),
("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"),
("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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Die Anfrage zur Bildschirmfreigabe auf dem entfernten Gerät endete, ohne abgeschlossen zu werden"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk konnte vom XDG Desktop Portal keinen nutzbaren Bildschirm erhalten, die PipeWire-Bibliothek ist möglicherweise zu alt"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk konnte eine für die Bildschirmaufnahme benötigte GStreamer-Komponente nicht laden ({})"),
("Relay fallback delay in seconds", "Verzögerung bis zum Relais in Sekunden"),
("relay-fallback-delay-tip", "Wie lange eine bereits aufgebaute Relaisverbindung auf die direkte WebRTC-Verbindung wartet, bevor sie stattdessen verwendet wird. Erhöhen Sie den Wert, um einer langsamen direkten Verbindung mehr Zeit zu geben; verringern Sie ihn, um in Netzwerken ohne mögliche Direktverbindung schneller auf das Relais zurückzufallen. Leer lassen für den Standardwert von 2.5 Sekunden."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Ενεργοποίηση σύνδεσης WebRTC P2P"),
("Enable TCP hole punching", "Ενεργοποίηση διάτρησης οπών TCP"),
("The screen sharing request was declined on the remote device", "Το αίτημα κοινής χρήσης οθόνης απορρίφθηκε στην απομακρυσμένη συσκευή"),
("No one responded to the screen sharing request on the remote device", "Κανείς δεν απάντησε στο αίτημα κοινής χρήσης οθόνης στην απομακρυσμένη συσκευή"),
("The XDG Desktop Portal ended the screen sharing request ({})", "Το XDG Desktop Portal τερμάτισε το αίτημα κοινής χρήσης οθόνης ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "Το XDG Desktop Portal δεν επέστρεψε οθόνη για καταγραφή, η βιβλιοθήκη PipeWire ίσως είναι πολύ παλιά"),
("A GStreamer plugin needed for screen capture is missing ({})", "Λείπει ένα πρόσθετο GStreamer που απαιτείται για την καταγραφή οθόνης ({})"),
("The screen sharing request timed out on the remote device", "Το αίτημα κοινής χρήσης οθόνης έληξε στην απομακρυσμένη συσκευή"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "Το RustDesk δεν μπορεί να προσεγγίσει τη συνεδρία επιφάνειας εργασίας στην απομακρυσμένη συσκευή, ελέγξτε ότι μια συνεδρία εκτελείται και ότι το RustDesk μπορεί να τη χρησιμοποιήσει"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Στην πύλη επιφάνειας εργασίας της απομακρυσμένης συσκευής λείπει μια δυνατότητα που απαιτείται για κοινή χρήση οθόνης ή απομακρυσμένο έλεγχο, ίσως δεν είναι εγκατεστημένο το υποσύστημά της"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Η κοινή χρήση οθόνης εγκρίθηκε στην απομακρυσμένη συσκευή, αλλά δεν ήταν δυνατό το άνοιγμα της σύνδεσης PipeWire"),
("The screen sharing request ended without completing on the remote device", "Το αίτημα κοινής χρήσης οθόνης στην απομακρυσμένη συσκευή έληξε χωρίς να ολοκληρωθεί"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "Το RustDesk δεν μπόρεσε να λάβει αξιοποιήσιμη οθόνη από το XDG Desktop Portal, η βιβλιοθήκη PipeWire ίσως είναι πολύ παλιά"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "Το RustDesk δεν μπόρεσε να φορτώσει ένα στοιχείο του GStreamer που απαιτείται για την καταγραφή οθόνης ({})"),
("Relay fallback delay in seconds", "Καθυστέρηση πριν από τη χρήση αναμεταδότη σε δευτερόλεπτα"),
("relay-fallback-delay-tip", "Πόσο χρόνο περιμένει μια ήδη ενεργή σύνδεση αναμεταδότη την απευθείας σύνδεση WebRTC πριν χρησιμοποιηθεί στη θέση της. Αυξήστε το για να δώσετε σε μια αργή απευθείας σύνδεση περισσότερο χρόνο. Μειώστε το για ταχύτερη επιστροφή στον αναμεταδότη σε δίκτυα όπου δεν είναι δυνατή η απευθείας σύνδεση. Αφήστε το κενό για την προεπιλογή των 2.5 δευτερολέπτων."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -84,10 +84,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_input_permission_tip1", "In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the \"Accessibility\" service."),
("android_input_permission_tip2", "Please go to the next system settings page, find and enter [Installed Services], turn on [RustDesk Input] service."),
("android_new_connection_tip", "New control request has been received, which wants to control your current device."),
("android_service_will_start_tip", "Turning on \"Screen Capture\" will automatically start the service, allowing other devices to request a connection to your device."),
("android_service_will_start_tip", "Turning on \"Screen capture\" will automatically start the service, allowing other devices to request a connection to your device."),
("android_stop_service_tip", "Closing the service will automatically close all established connections."),
("android_version_audio_tip", "The current Android version does not support audio capture, please upgrade to Android 10 or higher."),
("android_start_service_tip", "Tap [Start service] or enable [Screen Capture] permission to start the screen sharing service."),
("android_start_service_tip", "Tap [Start service] or enable [Screen capture] permission to start the screen sharing service."),
("android_permission_may_not_change_tip", "Permissions for established connections may not be changed instantly until reconnected."),
("doc_mac_permission", "https://rustdesk.com/docs/en/client/mac/#enable-permissions"),
("Ignore Battery Optimizations", "Ignore battery optimizations"),
@@ -252,11 +252,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("disable-udp-tip", "Controls whether to use TCP only.\nWhen this option enabled, RustDesk will not use UDP 21116 any more, TCP 21116 will be used instead."),
("server-oss-not-support-tip", "NOTE: RustDesk server OSS doesn't include this feature."),
("note-at-conn-end-tip", "Ask for note at end of connection"),
("rel-mouse-not-supported-peer-tip", "Relative Mouse Mode is not supported by the connected peer."),
("rel-mouse-not-ready-tip", "Relative Mouse Mode is not ready yet. Please try again."),
("rel-mouse-lock-failed-tip", "Failed to lock cursor. Relative Mouse Mode has been disabled."),
("rel-mouse-not-supported-peer-tip", "Relative mouse mode is not supported by the connected peer."),
("rel-mouse-not-ready-tip", "Relative mouse mode is not ready yet. Please try again."),
("rel-mouse-lock-failed-tip", "Failed to lock cursor. Relative mouse mode has been disabled."),
("rel-mouse-exit-{}-tip", "Press {} to exit."),
("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative Mouse Mode has been disabled."),
("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative mouse mode has been disabled."),
("keep-awake-during-outgoing-sessions-label", "Keep screen awake during outgoing sessions"),
("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"),
("password-hidden-tip", "Permanent password is set (hidden)."),
@@ -278,5 +278,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("sync-clipboard-between-sessions-tip", "Text or images copied in one remote session are also sent to the clipboard of your other connected sessions."),
("terminal-clipboard-write-tip", "An app in the terminal wants to copy text to this device's clipboard. If granted, this permission applies to terminal apps in all connections until you turn it off in Settings. Manual copy and paste are unaffected."),
("port-forward-mux-tip", "Carry every connection of a port-forward mapping over a single connection to the peer, instead of connecting and logging in again for each one."),
("relay-fallback-delay-tip", "How long a relay connection that is already up waits for the direct WebRTC connection before it is used instead. Raise it to give a slow direct connection more time to win; lower it to settle on the relay sooner on networks where a direct connection cannot be made. Leave empty for the default of 2.5 seconds."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Ebligi WebRTC P2P-konekton"),
("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"),
("No one responded to the screen sharing request on the remote device", "Neniu respondis al la peto pri ekrandividado sur la fora aparato"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal finis la peton pri ekrandividado ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Mankas kromprogramo de GStreamer necesa por ekrankapto ({})"),
("The screen sharing request timed out on the remote device", "La peto pri ekrandividado eltempiĝis 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "La peto pri ekrandividado sur la fora aparato finiĝis sen kompletiĝi"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk ne povis akiri uzeblan ekranon de XDG Desktop Portal, la biblioteko PipeWire eble estas tro malnova"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ne povis ŝargi komponanton de GStreamer necesan por ekrankapto ({})"),
("Relay fallback delay in seconds", "Prokrasto antaŭ retransmisio en sekundoj"),
("relay-fallback-delay-tip", "Kiom longe jam establita retransmisia konekto atendas la rektan WebRTC-konekton antaŭ ol esti uzata anstataŭe. Pligrandigu ĝin por doni al malrapida rekta konekto pli da tempo; malpligrandigu ĝin por pli frue uzi la retransmision en retoj kie rekta konekto ne eblas. Lasu malplena por la defaŭlta valoro de 2.5 sekundoj."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -282,7 +282,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_service_will_start_tip", "Habilitar la captura de pantalla iniciará automáticamente el servicio, lo que permitirá que otros dispositivos soliciten una conexión desde este dispositivo."),
("android_stop_service_tip", "Cerrar el servicio cerrará automáticamente todas las conexiones establecidas."),
("android_version_audio_tip", "La versión actual de Android no admite la captura de audio, actualice a Android 10 o posterior."),
("android_start_service_tip", "Toque [Iniciar servicio] o conceda el permiso [Captura de pantalla] para iniciar el servicio de pantalla compartida."),
("android_start_service_tip", "Toque [Iniciar Servicio] o conceda el permiso [Captura de pantalla] para iniciar el servicio de pantalla compartida."),
("android_permission_may_not_change_tip", "Es posible que los permisos de las conexiones ya establecidas no cambien de inmediato hasta que se vuelva a conectar."),
("Account", "Cuenta"),
("Overwrite", "Sobrescribir"),
@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Habilitar conexión WebRTC P2P"),
("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"),
("No one responded to the screen sharing request on the remote device", "Nadie respondió a la solicitud de compartir pantalla en el dispositivo remoto"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal finalizó la solicitud de compartir pantalla ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Falta un complemento de GStreamer necesario para capturar la pantalla ({})"),
("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"),
("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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "La solicitud de compartir pantalla en el dispositivo remoto terminó sin completarse"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk no ha podido obtener una pantalla utilizable del XDG Desktop Portal; la biblioteca PipeWire puede ser demasiado antigua"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk no ha podido cargar un componente de GStreamer necesario para capturar la pantalla ({})"),
("Relay fallback delay in seconds", "Retardo antes de usar el relé en segundos"),
("relay-fallback-delay-tip", "Cuánto tiempo espera una conexión de relé ya establecida a la conexión directa WebRTC antes de usarse en su lugar. Auméntelo para dar más tiempo a una conexión directa lenta; redúzcalo para recurrir antes al relé en redes donde no es posible una conexión directa. Déjelo vacío para el valor predeterminado de 2.5 segundos."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -187,7 +187,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enter your password", "Sisesta oma parool"),
("Logging in...", "Sisselogimine..."),
("Enable RDP session sharing", "Luba RDP-seansi jagamine"),
("Auto Login", "Logi automaatselt sisse (Kehtib vaid valiku \"lukusta pärast seansi lõppu\" lubamisel)"),
("Auto Login", "Logi automaatselt sisse (Kehtib vaid valiku \"Lukusta pärast seansi lõppu\" lubamisel)"),
("Enable direct IP access", "Luba otsene IP-juurdepääs"),
("Rename", "Nimeta ümber"),
("Space", "Ruum"),
@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Luba WebRTC P2P-ühendus"),
("Enable TCP hole punching", "Luba TCP-augustamine"),
("The screen sharing request was declined on the remote device", "Ekraani jagamise taotlus lükati kaugseadmes tagasi"),
("No one responded to the screen sharing request on the remote device", "Keegi ei vastanud kaugseadmes ekraani jagamise taotlusele"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal lõpetas ekraani jagamise taotluse ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Ekraani jäädvustamiseks vajalik GStreameri plugin puudub ({})"),
("The screen sharing request timed out on the remote device", "Ekraani jagamise taotlus aegus kaugseadmes"),
("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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Ekraani jagamise taotlus kaugseadmes lõppes ilma lõpule jõudmata"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk ei saanud XDG Desktop Portalilt kasutatavat ekraani, PipeWire'i teek võib olla liiga vana"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ei suutnud laadida ekraani jäädvustamiseks vajalikku GStreameri komponenti ({})"),
("Relay fallback delay in seconds", "Viivitus enne relee kasutamist sekundites"),
("relay-fallback-delay-tip", "Kui kaua juba loodud releeühendus ootab otsest WebRTC-ühendust, enne kui seda selle asemel kasutatakse. Suurendage, et anda aeglasele otseühendusele rohkem aega; vähendage, et võrkudes, kus otseühendust luua ei saa, releele kiiremini üle minna. Jätke tühjaks vaikeväärtuse 2.5 sekundit kasutamiseks."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Gaitu WebRTC P2P konexioa"),
("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"),
("No one responded to the screen sharing request on the remote device", "Inork ez du erantzun urruneko gailuko pantaila partekatzeko eskaerari"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal-ek pantaila partekatzeko eskaera amaitu du ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Pantaila kapturatzeko beharrezkoa den GStreamer plugina falta da ({})"),
("The screen sharing request timed out on the remote device", "Pantaila partekatzeko eskaerak denbora-muga gainditu du urruneko gailuan"),
("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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Urruneko gailuko pantaila partekatzeko eskaera osatu gabe amaitu da"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk-ek ezin izan du pantaila erabilgarririk lortu XDG Desktop Portal-etik, PipeWire liburutegia zaharregia izan daiteke"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk-ek ezin izan du pantaila kapturatzeko beharrezkoa den GStreamer osagai bat kargatu ({})"),
("Relay fallback delay in seconds", "Errelera itzultzeko atzerapena segundotan"),
("relay-fallback-delay-tip", "Dagoeneko ezarritako errele-konexio batek WebRTC konexio zuzenari zenbat denbora itxaroten dion, haren ordez erabili aurretik. Handitu konexio zuzen motel bati denbora gehiago emateko; txikitu konexio zuzena egin ezin den sareetan lehenago errelera itzultzeko. Utzi hutsik 2.5 segundoko balio lehenetsirako."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "فعال‌سازی اتصال همتا‌به‌همتای WebRTC"),
("Enable TCP hole punching", "فعال‌سازی تکنیک TCP hole punching"),
("The screen sharing request was declined on the remote device", "درخواست اشتراک‌گذاری صفحه در دستگاه راه دور رد شد"),
("No one responded to the screen sharing request on the remote device", "هیچ‌کس به درخواست اشتراک‌گذاری صفحه در دستگاه راه دور پاسخ نداد"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal درخواست اشتراک‌گذاری صفحه را پایان داد ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal هیچ صفحه‌ای برای ضبط بازنگرداند، ممکن است کتابخانه PipeWire خیلی قدیمی باشد"),
("A GStreamer plugin needed for screen capture is missing ({})", "افزونه GStreamer موردنیاز برای ضبط صفحه موجود نیست ({})"),
("The screen sharing request timed out on the remote device", "مهلت درخواست اشتراک‌گذاری صفحه در دستگاه راه دور به پایان رسید"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk نمی‌تواند به نشست میزکار دستگاه راه دور دسترسی پیدا کند، بررسی کنید که نشست میزکار در حال اجرا باشد و RustDesk بتواند از آن استفاده کند"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "درگاه میزکار در دستگاه راه دور قابلیت لازم برای اشتراک‌گذاری صفحه یا کنترل از راه دور را ندارد، شاید پیاده‌سازی آن نصب نشده باشد"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "اشتراک‌گذاری صفحه در دستگاه راه دور تأیید شد، اما اتصال PipeWire باز نشد"),
("The screen sharing request ended without completing on the remote device", "درخواست اشتراک‌گذاری صفحه در دستگاه راه دور بدون تکمیل شدن پایان یافت"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk نتوانست صفحه‌ای قابل استفاده از XDG Desktop Portal دریافت کند، ممکن است کتابخانه PipeWire خیلی قدیمی باشد"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk نتوانست مؤلفه GStreamer موردنیاز برای ضبط صفحه را بارگذاری کند ({})"),
("Relay fallback delay in seconds", "تأخیر بازگشت به رله بر حسب ثانیه"),
("relay-fallback-delay-tip", "یک اتصال رله که از قبل برقرار شده چقدر منتظر اتصال مستقیم WebRTC می ماند پیش از آنکه به جای آن استفاده شود. آن را افزایش دهید تا به اتصال مستقیم کند فرصت بیشتری داده شود؛ کاهش دهید تا در شبکه هایی که اتصال مستقیم ممکن نیست، زودتر به رله بازگردد. برای مقدار پیش فرض 2.5 ثانیه خالی بگذارید."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Ota WebRTC P2P yhteys käyttöön"),
("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"),
("No one responded to the screen sharing request on the remote device", "Kukaan ei vastannut näytön jakamispyyntöön etälaitteessa"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal päätti näytön jakamispyynnön ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Näytön kaappaukseen tarvittava GStreamer-liitännäinen puuttuu ({})"),
("The screen sharing request timed out on the remote device", "Näytön jakamispyyntö aikakatkaistiin 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Näytön jakamispyyntö etälaitteessa päättyi ilman että se saatiin valmiiksi"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk ei saanut XDG Desktop Portalilta käyttökelpoista näyttöä, PipeWire-kirjasto voi olla liian vanha"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ei voinut ladata näytön kaappaukseen tarvittavaa GStreamer-osaa ({})"),
("Relay fallback delay in seconds", "Viive ennen välitykseen siirtymistä sekunteina"),
("relay-fallback-delay-tip", "Kuinka kauan jo muodostettu välitysyhteys odottaa suoraa WebRTC-yhteyttä ennen kuin sitä käytetään sen sijaan. Kasvata arvoa antaaksesi hitaalle suoralle yhteydelle enemmän aikaa; pienennä sitä siirtyäksesi nopeammin välitykseen verkoissa, joissa suoraa yhteyttä ei voi muodostaa. Jätä tyhjäksi käyttääksesi oletusarvoa 2.5 sekuntia."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Activer la connexion P2P WebRTC"),
("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"),
("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"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal a mis fin à la demande de partage d'écran ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Un greffon GStreamer nécessaire à la capture d'écran est manquant ({})"),
("The screen sharing request timed out on the remote device", "La demande de partage d'écran a expiré 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 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é"),
("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"),
("The screen sharing request ended without completing on the remote device", "La demande de partage d'écran sur l'appareil distant s'est terminée sans aboutir"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk n'a pas pu obtenir d'écran exploitable auprès du XDG Desktop Portal, la bibliothèque PipeWire est peut-être trop ancienne"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk n'a pas pu charger un composant GStreamer nécessaire à la capture d'écran ({})"),
("Relay fallback delay in seconds", "Délai avant bascule vers le relais en secondes"),
("relay-fallback-delay-tip", "Durée pendant laquelle une connexion relais déjà établie attend la connexion directe WebRTC avant d'être utilisée à sa place. Augmentez-la pour laisser plus de temps à une connexion directe lente ; diminuez-la pour basculer plus tôt vers le relais sur les réseaux où une connexion directe est impossible. Laissez vide pour la valeur par défaut de 2.5 secondes."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P კავშირის ჩართვა"),
("Enable TCP hole punching", "TCP hole punching-ის ჩართვა"),
("The screen sharing request was declined on the remote device", "ეკრანის გაზიარების მოთხოვნა უარყოფილია დისტანციურ მოწყობილობაზე"),
("No one responded to the screen sharing request on the remote device", "დისტანციურ მოწყობილობაზე ეკრანის გაზიარების მოთხოვნას არავინ უპასუხა"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal-მა დაასრულა ეკრანის გაზიარების მოთხოვნა ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal-მა არ დააბრუნა ჩასაწერი ეკრანი, PipeWire-ის ბიბლიოთეკა შესაძლოა ძალიან ძველია"),
("A GStreamer plugin needed for screen capture is missing ({})", "ეკრანის ჩაწერისთვის საჭირო GStreamer-ის მოდული აკლია ({})"),
("The screen sharing request timed out on the remote device", "ეკრანის გაზიარების მოთხოვნას ვადა გაუვიდა დისტანციურ მოწყობილობაზე"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk-ს არ შეუძლია დისტანციური მოწყობილობის სამუშაო მაგიდის სესიასთან წვდომა, შეამოწმეთ, რომ სესია გაშვებულია და RustDesk-ს შეუძლია მისი გამოყენება"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "დისტანციური მოწყობილობის სამუშაო მაგიდის პორტალს აკლია ეკრანის გაზიარებისთვის ან დისტანციური მართვისთვის საჭირო შესაძლებლობა, შესაძლოა მისი ბექენდი დაინსტალირებული არ არის"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "ეკრანის გაზიარება დამტკიცდა დისტანციურ მოწყობილობაზე, მაგრამ PipeWire-ის კავშირის გახსნა ვერ მოხერხდა"),
("The screen sharing request ended without completing on the remote device", "ეკრანის გაზიარების მოთხოვნა დისტანციურ მოწყობილობაზე დასრულდა შეუსრულებლად"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk-მა ვერ მიიღო გამოსადეგი ეკრანი XDG Desktop Portal-იდან, PipeWire-ის ბიბლიოთეკა შესაძლოა ძალიან ძველია"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk-მა ვერ ჩატვირთა ეკრანის ჩაწერისთვის საჭირო GStreamer-ის კომპონენტი ({})"),
("Relay fallback delay in seconds", "რელეზე გადასვლის დაყოვნება წამებში"),
("relay-fallback-delay-tip", "რამდენ ხანს ელოდება უკვე დამყარებული რელე-კავშირი პირდაპირ WebRTC კავშირს, სანამ მის ნაცვლად გამოიყენება. გაზარდეთ, რომ ნელ პირდაპირ კავშირს მეტი დრო მისცეთ; შეამცირეთ, რომ ქსელებში, სადაც პირდაპირი კავშირი შეუძლებელია, უფრო სწრაფად გადავიდეს რელეზე. დატოვეთ ცარიელი ნაგულისხმევი 2.5 წამისთვის."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Activar conexión P2P por WebRTC"),
("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"),
("No one responded to the screen sharing request on the remote device", "Ninguén respondeu á solicitude de compartir pantalla no dispositivo remoto"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal rematou a solicitude de compartir pantalla ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Falta un complemento de GStreamer necesario para capturar a pantalla ({})"),
("The screen sharing request timed out on the remote device", "A solicitude de compartir pantalla esgotou o tempo 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "A solicitude de compartir pantalla no dispositivo remoto rematou sen completarse"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk non puido obter unha pantalla utilizable do XDG Desktop Portal, a biblioteca PipeWire pode ser demasiado antiga"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk non puido cargar un compoñente de GStreamer necesario para capturar a pantalla ({})"),
("Relay fallback delay in seconds", "Atraso antes de usar o relé en segundos"),
("relay-fallback-delay-tip", "Canto tempo agarda unha conexión de relé xa establecida pola conexión directa WebRTC antes de usarse no seu lugar. Auménteo para darlle máis tempo a unha conexión directa lenta; redúzao para recorrer antes ao relé en redes onde non é posible unha conexión directa. Déixeo baleiro para o valor predeterminado de 2.5 segundos."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P કનેક્શન સક્ષમ કરો"),
("Enable TCP hole punching", "TCP હોલ પંચિંગ સક્ષમ કરો"),
("The screen sharing request was declined on the remote device", "રિમોટ ઉપકરણ પર સ્ક્રીન શેરિંગ વિનંતી નકારવામાં આવી"),
("No one responded to the screen sharing request on the remote device", "રિમોટ ઉપકરણ પર સ્ક્રીન શેરિંગ વિનંતીનો કોઈએ જવાબ આપ્યો નથી"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal એ સ્ક્રીન શેરિંગ વિનંતી સમાપ્ત કરી ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal એ કૅપ્ચર કરવા માટે કોઈ સ્ક્રીન પરત કરી નથી, PipeWire લાઇબ્રેરી કદાચ ઘણી જૂની છે"),
("A GStreamer plugin needed for screen capture is missing ({})", "સ્ક્રીન કૅપ્ચર માટે જરૂરી GStreamer પ્લગઇન ખૂટે છે ({})"),
("The screen sharing request timed out on the remote device", "રિમોટ ઉપકરણ પર સ્ક્રીન શેરિંગ વિનંતીનો સમય સમાપ્ત થયો"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk રિમોટ ઉપકરણના ડેસ્કટોપ સત્ર સુધી પહોંચી શકતું નથી, ખાતરી કરો કે ડેસ્કટોપ સત્ર ચાલુ છે અને RustDesk તેનો ઉપયોગ કરી શકે છે"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "રિમોટ ઉપકરણ પરના ડેસ્કટોપ પોર્ટલમાં સ્ક્રીન શેરિંગ અથવા રિમોટ કંટ્રોલ માટે જરૂરી ક્ષમતા નથી, તેનું બેકએન્ડ કદાચ ઇન્સ્ટોલ કરેલું નથી"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "રિમોટ ઉપકરણ પર સ્ક્રીન શેરિંગ મંજૂર થયું, પરંતુ PipeWire કનેક્શન ખોલી શકાયું નહીં"),
("The screen sharing request ended without completing on the remote device", "રિમોટ ઉપકરણ પર સ્ક્રીન શેરિંગ વિનંતી પૂર્ણ થયા વિના સમાપ્ત થઈ"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk XDG Desktop Portal પાસેથી ઉપયોગી સ્ક્રીન મેળવી શક્યું નથી, PipeWire લાઇબ્રેરી કદાચ ઘણી જૂની છે"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk સ્ક્રીન કૅપ્ચર માટે જરૂરી GStreamer ઘટક લોડ કરી શક્યું નથી ({})"),
("Relay fallback delay in seconds", "રિલે પર પાછા ફરવામાં વિલંબ સેકન્ડમાં"),
("relay-fallback-delay-tip", "પહેલેથી સ્થાપિત રિલે કનેક્શન સીધા WebRTC કનેક્શનની કેટલો સમય રાહ જુએ છે, ત્યાર બાદ તેના બદલે વપરાય છે. ધીમા સીધા કનેક્શનને વધુ સમય આપવા માટે વધારો; જ્યાં સીધું કનેક્શન શક્ય નથી તેવા નેટવર્ક પર વહેલા રિલે પર જવા માટે ઘટાડો. મૂળભૂત 2.5 સેકન્ડ માટે ખાલી રાખો."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "אפשר חיבור WebRTC P2P"),
("Enable TCP hole punching", "אפשר TCP hole punching"),
("The screen sharing request was declined on the remote device", "בקשת שיתוף המסך נדחתה במכשיר המרוחק"),
("No one responded to the screen sharing request on the remote device", "איש לא הגיב לבקשת שיתוף המסך במכשיר המרוחק"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal סיים את בקשת שיתוף המסך ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal לא החזיר מסך ללכידה, ייתכן שספריית PipeWire ישנה מדי"),
("A GStreamer plugin needed for screen capture is missing ({})", "חסר תוסף GStreamer הדרוש ללכידת מסך ({})"),
("The screen sharing request timed out on the remote device", "תם הזמן המוקצב לבקשת שיתוף המסך במכשיר המרוחק"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk אינו יכול לגשת להפעלת שולחן העבודה במכשיר המרוחק, ודאו שההפעלה פועלת ושRustDesk יכול להשתמש בה"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "לפורטל שולחן העבודה במכשיר המרוחק חסרה יכולת הדרושה לשיתוף מסך או לשליטה מרחוק, ייתכן שהמימוש שלו אינו מותקן"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "שיתוף המסך אושר במכשיר המרוחק, אך לא ניתן היה לפתוח את חיבור PipeWire"),
("The screen sharing request ended without completing on the remote device", "בקשת שיתוף המסך במכשיר המרוחק הסתיימה מבלי להתבצע"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk לא הצליח לקבל מסך שמיש מ-XDG Desktop Portal, ייתכן שספריית PipeWire ישנה מדי"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk לא הצליח לטעון רכיב GStreamer הדרוש ללכידת מסך ({})"),
("Relay fallback delay in seconds", "השהיה לפני מעבר לממסר בשניות"),
("relay-fallback-delay-tip", "כמה זמן חיבור ממסר שכבר נוצר ממתין לחיבור WebRTC הישיר לפני שישמש במקומו. הגדל כדי לתת לחיבור ישיר איטי יותר זמן; הקטן כדי לעבור מהר יותר לממסר ברשתות שבהן לא ניתן ליצור חיבור ישיר. השאר ריק לערך ברירת המחדל של 2.5 שניות."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P कनेक्शन सक्षम करें"),
("Enable TCP hole punching", "TCP होल पंचिंग सक्षम करें"),
("The screen sharing request was declined on the remote device", "रिमोट डिवाइस पर स्क्रीन शेयरिंग अनुरोध अस्वीकार कर दिया गया"),
("No one responded to the screen sharing request on the remote device", "रिमोट डिवाइस पर स्क्रीन शेयरिंग अनुरोध का किसी ने उत्तर नहीं दिया"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal ने स्क्रीन शेयरिंग अनुरोध समाप्त कर दिया ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal ने कैप्चर करने के लिए कोई स्क्रीन नहीं लौटाई, PipeWire लाइब्रेरी बहुत पुरानी हो सकती है"),
("A GStreamer plugin needed for screen capture is missing ({})", "स्क्रीन कैप्चर के लिए आवश्यक GStreamer प्लगइन अनुपस्थित है ({})"),
("The screen sharing request timed out on the remote device", "रिमोट डिवाइस पर स्क्रीन शेयरिंग अनुरोध का समय समाप्त हो गया"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk रिमोट डिवाइस के डेस्कटॉप सत्र तक नहीं पहुँच सकता, जाँचें कि डेस्कटॉप सत्र चल रहा है और RustDesk उसका उपयोग कर सकता है"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "रिमोट डिवाइस के डेस्कटॉप पोर्टल में स्क्रीन शेयरिंग या रिमोट कंट्रोल के लिए आवश्यक क्षमता नहीं है, शायद उसका बैकएंड इंस्टॉल नहीं है"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "रिमोट डिवाइस पर स्क्रीन शेयरिंग स्वीकृत हुई, लेकिन PipeWire कनेक्शन नहीं खोला जा सका"),
("The screen sharing request ended without completing on the remote device", "रिमोट डिवाइस पर स्क्रीन शेयरिंग अनुरोध पूरा हुए बिना समाप्त हो गया"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk XDG Desktop Portal से उपयोग योग्य स्क्रीन प्राप्त नहीं कर सका, PipeWire लाइब्रेरी बहुत पुरानी हो सकती है"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk स्क्रीन कैप्चर के लिए आवश्यक GStreamer घटक लोड नहीं कर सका ({})"),
("Relay fallback delay in seconds", "रिले पर लौटने में विलंब सेकंड में"),
("relay-fallback-delay-tip", "पहले से स्थापित रिले कनेक्शन सीधे WebRTC कनेक्शन की कितनी देर प्रतीक्षा करता है, उसके बाद उसके स्थान पर उपयोग किया जाता है। धीमे सीधे कनेक्शन को अधिक समय देने के लिए बढ़ाएँ; जिन नेटवर्क पर सीधा कनेक्शन संभव नहीं है वहाँ जल्दी रिले पर जाने के लिए घटाएँ। डिफ़ॉल्ट 2.5 सेकंड के लिए खाली छोड़ें।"),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Omogući WebRTC P2P vezu"),
("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"),
("No one responded to the screen sharing request on the remote device", "Nitko nije odgovorio na zahtjev za dijeljenje zaslona na udaljenom uređaju"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal prekinuo je zahtjev za dijeljenje zaslona ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Nedostaje GStreamer dodatak potreban za snimanje zaslona ({})"),
("The screen sharing request timed out on the remote device", "Zahtjev za dijeljenje zaslona istekao je 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Zahtjev za dijeljenje zaslona na udaljenom uređaju završio je bez dovršetka"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nije mogao dobiti upotrebljiv zaslon od XDG Desktop Portala, PipeWire biblioteka je možda prestara"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nije mogao učitati GStreamer komponentu potrebnu za snimanje zaslona ({})"),
("Relay fallback delay in seconds", "Odgoda prije prelaska na relej u sekundama"),
("relay-fallback-delay-tip", "Koliko dugo već uspostavljena relejna veza čeka izravnu WebRTC vezu prije nego što se upotrijebi umjesto nje. Povećajte da sporoj izravnoj vezi date više vremena; smanjite da se na mrežama gdje izravna veza nije moguća brže prijeđe na relej. Ostavite prazno za zadanu vrijednost od 2.5 sekunde."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P kapcsolat 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"),
("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"),
("The XDG Desktop Portal ended the screen sharing request ({})", "Az XDG Desktop Portal befejezte a képernyőmegosztási kérést ({})"),
("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"),
("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 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"),
("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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "A képernyőmegosztási kérés a távoli eszközön befejeződött anélkül, hogy teljesült volna"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "A RustDesk nem kapott használható képernyőt az XDG Desktop Portaltól, a PipeWire programkönyvtár túl régi lehet"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "A RustDesk nem tudta betölteni a képernyőrögzítéshez szükséges GStreamer összetevőt ({})"),
("Relay fallback delay in seconds", "Késleltetés a továbbítóra váltás előtt másodpercben"),
("relay-fallback-delay-tip", "Mennyi ideig vár a már létrejött továbbító kapcsolat a közvetlen WebRTC kapcsolatra, mielőtt helyette használnák. Növelje, hogy a lassú közvetlen kapcsolatnak több ideje legyen; csökkentse, hogy olyan hálózatokon, ahol közvetlen kapcsolat nem hozható létre, hamarabb váltson továbbítóra. Hagyja üresen az alapértelmezett 2.5 másodperchez."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -716,11 +716,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("note-at-conn-end-tip", "Minta catatan di akhir koneksi"),
("Show terminal extra keys", "Tampilkan tombol tambahan terminal"),
("Relative mouse mode", "Mode mouse relatif"),
("rel-mouse-not-supported-peer-tip", "Mode Mouse Relatif tidak didukung oleh peer yang terhubung."),
("rel-mouse-not-ready-tip", "Mode Mouse Relatif belum siap. Silakan coba lagi."),
("rel-mouse-lock-failed-tip", "Gagal mengunci kursor. Mode Mouse Relatif telah dinonaktifkan."),
("rel-mouse-not-supported-peer-tip", "Mode mouse relatif tidak didukung oleh peer yang terhubung."),
("rel-mouse-not-ready-tip", "Mode mouse relatif belum siap. Silakan coba lagi."),
("rel-mouse-lock-failed-tip", "Gagal mengunci kursor. Mode mouse relatif telah dinonaktifkan."),
("rel-mouse-exit-{}-tip", "Tekan {} untuk keluar."),
("rel-mouse-permission-lost-tip", "Izin keyboard dicabut. Mode Mouse Relatif telah dinonaktifkan."),
("rel-mouse-permission-lost-tip", "Izin keyboard dicabut. Mode mouse relatif telah dinonaktifkan."),
("Changelog", "Catatan perubahan"),
("keep-awake-during-outgoing-sessions-label", "Jaga layar tetap menyala selama sesi keluar"),
("keep-awake-during-incoming-sessions-label", "Jaga layar tetap menyala selama sesi masuk"),
@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Aktifkan koneksi P2P WebRTC"),
("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"),
("No one responded to the screen sharing request on the remote device", "Tidak ada yang menanggapi permintaan berbagi layar di perangkat jarak jauh"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal mengakhiri permintaan berbagi layar ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Plugin GStreamer yang diperlukan untuk merekam layar tidak ditemukan ({})"),
("The screen sharing request timed out on the remote device", "Permintaan berbagi layar kehabisan waktu 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Permintaan berbagi layar di perangkat jarak jauh berakhir tanpa diselesaikan"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk tidak mendapatkan layar yang dapat digunakan dari XDG Desktop Portal, pustaka PipeWire mungkin terlalu lama"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk tidak dapat memuat komponen GStreamer yang diperlukan untuk merekam layar ({})"),
("Relay fallback delay in seconds", "Jeda sebelum beralih ke relai dalam detik"),
("relay-fallback-delay-tip", "Berapa lama koneksi relai yang sudah terbentuk menunggu koneksi langsung WebRTC sebelum digunakan sebagai gantinya. Perbesar untuk memberi koneksi langsung yang lambat lebih banyak waktu; perkecil agar lebih cepat beralih ke relai pada jaringan yang tidak memungkinkan koneksi langsung. Biarkan kosong untuk nilai bawaan 2.5 detik."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -541,7 +541,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Plug out all", "Scollega tutto"),
("True color (4:4:4)", "Colore reale (4:4:4)"),
("Enable blocking user input", "Abilita blocco input utente"),
("id_input_tip", "Puoi inserire un ID, un IP diretto o un dominio con una porta (<dominio>:<porta>).\nSe vuoi accedere as un dispositivo in un altro server, aggiungi l'indirizzo del server (<id>@<indirizzo_server >?key=<valore_chiave>), ad esempio\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere as un dispositivo in un server pubblico, inserisci \"<id>@public\", per il server pubblico la chiave non è necessaria\n\nSe vuoi forzare l'uso di una connessione di inoltro alla prima connessione, aggiungi \"/r\" alla fine dell'ID, ad esempio \"9123456234/r\"."),
("id_input_tip", "Puoi inserire un ID, un IP diretto o un dominio con una porta (<dominio>:<porta>).\nSe vuoi accedere a un dispositivo in un altro server, aggiungi l'indirizzo del server (<id>@<indirizzo_server >?key=<valore_chiave>), ad esempio\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere as un dispositivo in un server pubblico, inserisci \"<id>@public\", per il server pubblico la chiave non è necessaria.\n\nSe vuoi forzare l'uso di una connessione di inoltro alla prima connessione, aggiungi \"/r\" alla fine dell'ID, ad esempio \"9123456234/r\"."),
("privacy_mode_impl_mag_tip", "Modo 1"),
("privacy_mode_impl_virtual_display_tip", "Modo 2"),
("Enter privacy mode", "Entra in modalità privacy"),
@@ -622,7 +622,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", "Cartella principale"),
("Resume", "Riprendi"),
("Invalid file name", "Nome file non valido"),
("one-way-file-transfer-tip", "Sul lato controllato è abilitato il trasferimento file unidirezionale."),
("one-way-file-transfer-tip", "Sul lato controllato è abilitato il trasferimento file unidirezionale."),
("Authentication Required", "Richiesta autenticazione"),
("Authenticate", "Autentica"),
("web_id_input_tip", "È possibile inserire un ID nello stesso server, nel client web non è supportato l'accesso con IP diretto.\nSe vuoi accedere ad un dispositivo in un altro server, aggiungi l'indirizzo del server (<id>@<indirizzo_server>?key=<valore_chiave >), ad esempio,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere ad un dispositivo in un server pubblico, inserisci \"<id>@public\", la chiave non è necessaria per il server pubblico."),
@@ -676,7 +676,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Default trackpad speed", "Velocità predefinita trackpad"),
("Numeric one-time password", "Password numerica monouso"),
("Enable IPv6 P2P connection", "Abilita connessione P2P IPv6"),
("Enable UDP hole punching", "Abilita hole punching UDP"),
("Enable UDP hole punching", "Abilita hole punching UDP"),
("View camera", "Visualizza telecamera"),
("Enable camera", "Abilita camera"),
("No cameras", "Nessuna camera"),
@@ -769,11 +769,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Reuse one connection for port forwarding", "Per l'inoltro delle porte riusa una sola connessione "),
("port-forward-mux-tip", "Fa passare tutte le connessioni di un inoltro porte in un'unica connessione verso il dispositivo remoto, invece di connettersi e autenticarsi di nuovo per ognuna."),
("Enable WebRTC P2P connection", "Abilita connessione P2P WebRTC"),
("Enable TCP hole punching", "Abilita hole punching TCP"),
("The screen sharing request was declined on the remote device", ""),
("No one responded to the screen sharing request on the remote device", ""),
("The XDG Desktop Portal ended the screen sharing request ({})", ""),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", ""),
("A GStreamer plugin needed for screen capture is missing ({})", ""),
("Enable TCP hole punching", "Abilita hole punching TCP"),
("The screen sharing request was declined on the remote device", "La richiesta di condivisione dello schermo nel dispositivo remoto è stata rifiutata"),
("The screen sharing request timed out on the remote device", "La richiesta di condivisione dello schermo nel dispositivo remoto è scaduta"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk non riesce a raggiungere la sessione desktop nel dispositivo remoto, controlla che sia in esecuzione una sessione desktop e che RustDesk possa usarla"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Nel portale desktop nel dispositivo remoto manca una funzionalità necessaria per la condivisione dello schermo o il controllo remoto, il relativo backend potrebbe non essere installato"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "La condivisione dello schermo è stata approvata nel dispositivo remoto, ma non è stato possibile aprire la connessione PipeWire"),
("The screen sharing request ended without completing on the remote device", "La richiesta di condivisione dello schermo si è chiusa senza essere completata nel dispositivo remoto"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk non è riuscito a ottenere una schermata usabile dal portale desktop XDG, la libreria PipeWire potrebbe essere troppo vecchia"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk non è riuscito a caricare un componente GStreamer necessario per l'acquisizione dello schermo ({})"),
("Relay fallback delay in seconds", "Ritardo fallback relay (secondi)"),
("relay-fallback-delay-tip", "Quanto tempo una connessione relay già attiva attende la connessione WebRTC diretta prima di essere usata. Aumentalo per dare a una connessione diretta lenta più tempo per funzionare; diminuiscilo per passare prima al relay sulle reti in cui non è possibile effettuare una connessione diretta. Lascia vuoto per il valore predefinito di 2,5 secondi."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "Per avviare una chiamata vocale, attiva nella pagina 'Condivisione schermo' la voce 'Cattura audio'.")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P 接続を有効化する"),
("Enable TCP hole punching", "TCP ホールパンチを有効化する"),
("The screen sharing request was declined on the remote device", "リモート端末で画面共有の要求が拒否されました"),
("No one responded to the screen sharing request on the remote device", "リモート端末で画面共有の要求に誰も応答しませんでした"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal が画面共有の要求を終了しました ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal がキャプチャ対象の画面を返しませんでした。PipeWire ライブラリが古すぎる可能性があります"),
("A GStreamer plugin needed for screen capture is missing ({})", "画面キャプチャに必要な GStreamer プラグインがありません ({})"),
("The screen sharing request timed out on the remote device", "リモート端末で画面共有の要求がタイムアウトしました"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk はリモート端末のデスクトップセッションにアクセスできません。セッションが動作していて RustDesk から利用できることを確認してください"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "リモート端末のデスクトップポータルに画面共有または遠隔操作に必要な機能がありません。バックエンドが未インストールの可能性があります"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "リモート端末で画面共有は許可されましたが、PipeWire 接続を開けませんでした"),
("The screen sharing request ended without completing on the remote device", "リモート端末での画面共有の要求は完了しないまま終了しました"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk は XDG Desktop Portal から使用可能な画面を取得できませんでした。PipeWire ライブラリが古すぎる可能性があります"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk は画面キャプチャに必要な GStreamer コンポーネントを読み込めませんでした ({})"),
("Relay fallback delay in seconds", "中継に切り替えるまでの待ち時間 (秒)"),
("relay-fallback-delay-tip", "すでに確立された中継接続が、直接の WebRTC 接続をどれだけ待ってから代わりに使用されるかを指定します。値を大きくすると遅い直接接続に時間を与えられ、小さくすると直接接続できないネットワークで早く中継に切り替わります。空欄にすると既定値の 2.5 秒になります。"),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -759,21 +759,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("id_whitelist_caveat_tip", "ID는 연결하는 클라이언트가 보고합니다. 화이트리스트는 노출을 줄이는 것으로 비밀번호나 2FA를 대체하지 않습니다"),
("whitelist_cidr_tip", "CIDR 표기를 지원합니다. 예: 192.168.1.0/24"),
("Continue", "계속"),
("Browser didn't open? Use the url below to sign in.", "브라우저가 열리지 않았나요? 아래 URL 로그인하세요."),
("Browser didn't open? Use the url below to sign in.", "브라우저가 열리지 않았나요? 아래 URL을 사용하여 로그인하세요."),
("Lock canvas", "캔버스 잠금"),
("Sync clipboard between sessions", "세션 간 클립보드 동기화"),
("sync-clipboard-between-sessions-tip", "하나의 원격 세션에서 복사한 텍스트나 이미지는 연결된 다른 세션의 클립보드도 전송됩니다."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("sync-clipboard-between-sessions-tip", "원격 세션 하나에서 복사한 텍스트나 이미지는 다른 연결된 세션의 클립보드도 전송됩니다."),
("terminal-clipboard-write-tip", "터미널의 앱이 이 장치의 클립보드에 텍스트를 복사하려고 합니다. 이 권한이 부여된 경우, 설정에서 이를 끌 때까지 모든 연결의 터미널 앱에 적용됩니다. 수동 복사 및 붙여넣기는 영향을 받지 않습니다."),
("Allow terminal apps to copy to clipboard", "터미널 앱이 클립보드로 복사하도록 허용"),
("Enable", "활성화"),
("Reuse one connection for port forwarding", "포트 포워딩에 연결 하나를 재사용"),
("port-forward-mux-tip", "포트 포워딩 하나의 모든 연결을 상대방과의 단일 연결로 전달합니다. 연결마다 다시 접속하고 로그인하지 않습니다."),
("Enable WebRTC P2P connection", "WebRTC P2P 연결 사용"),
("Enable TCP hole punching", "TCP 홀 펀칭 사용"),
("Reuse one connection for port forwarding", "포트 포워딩을 위해 하나의 연결을 재사용"),
("port-forward-mux-tip", "각 연결마다 다시 연결하고 로그인할 필요 없이, 포트 포워딩 매핑의 모든 연결을 피어에 대한 단일 연결로 전달합니다."),
("Enable WebRTC P2P connection", "WebRTC P2P 연결 사용"),
("Enable TCP hole punching", "TCP 홀 펀칭 사용"),
("The screen sharing request was declined on the remote device", "원격 장치에서 화면 공유 요청이 거부되었습니다"),
("No one responded to the screen sharing request on the remote device", "원격 장치에서 아무도 화면 공유 요청에 응답하지 않았습니다"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal이 화면 공유 요청을 종료했습니다 ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal이 캡처할 화면을 반환하지 않았습니다. PipeWire 라이브러리가 너무 오래되었을 수 있습니다"),
("A GStreamer plugin needed for screen capture is missing ({})", "화면 캡처에 필요한 GStreamer 플러그인이 없습니다 ({})"),
("The screen sharing request timed out on the remote device", "원격 장치에서 화면 공유 요청이 시간 초과되었습니다"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk가 원격 장치의 데스크톱 세션에 접근할 수 없습니다. 세션이 실행 중이고 RustDesk가 사용할 수 있는지 확인하세요"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "원격 장치의 데스크톱 포털에 화면 공유 또는 원격 제어에 필요한 기능이 없습니다. 백엔드가 설치되지 않았을 수 있습니다"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "원격 장치에서 화면 공유가 승인되었지만 PipeWire 연결을 열 수 없습니다"),
("The screen sharing request ended without completing on the remote device", "원격 장치의 화면 공유 요청이 완료되지 않은 채 종료되었습니다"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk가 XDG Desktop Portal에서 사용 가능한 화면을 가져오지 못했습니다. PipeWire 라이브러리가 너무 오래되었을 수 있습니다"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk가 화면 캡처에 필요한 GStreamer 구성 요소를 불러오지 못했습니다 ({})"),
("Relay fallback delay in seconds", "릴레이 대체 작동 지연 시간 (초)"),
("relay-fallback-delay-tip", "이미 연결된 중계 연결이 직접 WebRTC 연결을 얼마나 기다린 후 대신 사용되는지입니다. 값을 늘리면 느린 직접 연결에 더 많은 시간을 주고, 줄이면 직접 연결이 불가능한 네트워크에서 더 빨리 중계로 전환합니다. 비워 두면 기본값 2.5초가 사용됩니다."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "음성 통화를 시작하려면 '화면 공유' 페이지에서 '오디오 캡처'를 사용함으로 하세요.")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P қосылымын іске қосу"),
("Enable TCP hole punching", "TCP hole punching'ті іске қосу"),
("The screen sharing request was declined on the remote device", "Қашықтағы құрылғыда экранды бөлісу сұрауы қабылданбады"),
("No one responded to the screen sharing request on the remote device", "Қашықтағы құрылғыда экранды бөлісу сұрауына ешкім жауап бермеді"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal экранды бөлісу сұрауын аяқтады ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal түсіруге арналған экран қайтармады, PipeWire кітапханасы тым ескі болуы мүмкін"),
("A GStreamer plugin needed for screen capture is missing ({})", "Экранды түсіру үшін қажет GStreamer плагині жоқ ({})"),
("The screen sharing request timed out on the remote device", "Қашықтағы құрылғыда экранды бөлісу сұрауының уақыты бітті"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk қашықтағы құрылғының жұмыс үстелі сеансына қол жеткізе алмайды, сеанстың іске қосылғанын және RustDesk оны пайдалана алатынын тексеріңіз"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Қашықтағы құрылғының жұмыс үстелі порталында экранды бөлісуге немесе қашықтан басқаруға қажет мүмкіндік жоқ, оның бэкенді орнатылмаған болуы мүмкін"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Қашықтағы құрылғыда экранды бөлісуге рұқсат берілді, бірақ PipeWire байланысын ашу мүмкін болмады"),
("The screen sharing request ended without completing on the remote device", "Қашықтағы құрылғыдағы экранды бөлісу сұрауы аяқталмай тоқтады"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk XDG Desktop Portal-дан жарамды экран ала алмады, PipeWire кітапханасы тым ескі болуы мүмкін"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk экранды түсіру үшін қажет GStreamer компонентін жүктей алмады ({})"),
("Relay fallback delay in seconds", "Релеге ауысу кідірісі, секундпен"),
("relay-fallback-delay-tip", "Бұрыннан орнатылған реле байланысы тікелей WebRTC байланысын қанша уақыт күтеді, содан кейін оның орнына қолданылады. Баяу тікелей байланысқа көбірек уақыт беру үшін үлкейтіңіз; тікелей байланыс мүмкін емес желілерде релеге тезірек ауысу үшін кішірейтіңіз. Әдепкі 2.5 секунд үшін бос қалдырыңыз."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Įgalinti WebRTC P2P ryšį"),
("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"),
("No one responded to the screen sharing request on the remote device", "Niekas neatsakė į ekrano bendrinimo užklausą nuotoliniame įrenginyje"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal užbaigė ekrano bendrinimo užklausą ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Trūksta ekrano įrašymui reikalingo GStreamer papildinio ({})"),
("The screen sharing request timed out on the remote device", "Baigėsi ekrano bendrinimo užklausos laikas 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 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ė"),
("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"),
("The screen sharing request ended without completing on the remote device", "Ekrano bendrinimo užklausa nuotoliniame įrenginyje baigėsi jos neužbaigus"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk negavo tinkamo ekrano iš XDG Desktop Portal, PipeWire biblioteka gali būti per sena"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nepavyko įkelti ekrano įrašymui reikalingo GStreamer komponento ({})"),
("Relay fallback delay in seconds", "Delsa prieš pereinant prie perdavimo sekundėmis"),
("relay-fallback-delay-tip", "Kiek laiko jau užmegztas perdavimo ryšys laukia tiesioginio WebRTC ryšio, kol bus panaudotas vietoj jo. Padidinkite, kad lėtam tiesioginiam ryšiui būtų skirta daugiau laiko; sumažinkite, kad tinkluose, kuriuose tiesioginis ryšys neįmanomas, greičiau būtų pereinama prie perdavimo. Palikite tuščią numatytajai 2.5 sekundės reikšmei."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Iespējot WebRTC P2P savienojumu"),
("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"),
("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ē"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal pārtrauca ekrāna koplietošanas pieprasījumu ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Trūkst ekrāna tveršanai nepieciešamā GStreamer spraudņa ({})"),
("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"),
("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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Ekrāna koplietošanas pieprasījums attālinātajā ierīcē beidzās, netiekot pabeigts"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk neieguva izmantojamu ekrānu no XDG Desktop Portal, PipeWire bibliotēka var būt pārāk veca"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nevarēja ielādēt ekrāna tveršanai nepieciešamo GStreamer komponentu ({})"),
("Relay fallback delay in seconds", "Aizkave pirms pārslēgšanās uz retranslatoru sekundēs"),
("relay-fallback-delay-tip", "Cik ilgi jau izveidots retranslatora savienojums gaida tiešo WebRTC savienojumu, pirms tiek izmantots tā vietā. Palieliniet, lai lēnam tiešajam savienojumam dotu vairāk laika; samaziniet, lai tīklos, kur tiešais savienojums nav iespējams, ātrāk pārslēgtos uz retranslatoru. Atstājiet tukšu noklusējuma 2.5 sekunžu vērtībai."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P കണക്ഷൻ അനുവദിക്കുക"),
("Enable TCP hole punching", "TCP ഹോൾ പഞ്ചിംഗ് അനുവദിക്കുക"),
("The screen sharing request was declined on the remote device", "വിദൂര ഉപകരണത്തിൽ സ്ക്രീൻ പങ്കിടൽ അഭ്യർത്ഥന നിരസിച്ചു"),
("No one responded to the screen sharing request on the remote device", "വിദൂര ഉപകരണത്തിലെ സ്ക്രീൻ പങ്കിടൽ അഭ്യർത്ഥനയോട് ആരും പ്രതികരിച്ചില്ല"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal സ്ക്രീൻ പങ്കിടൽ അഭ്യർത്ഥന അവസാനിപ്പിച്ചു ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal പകർത്താൻ ഒരു സ്ക്രീനും നൽകിയില്ല, PipeWire ലൈബ്രറി വളരെ പഴയതാകാം"),
("A GStreamer plugin needed for screen capture is missing ({})", "സ്ക്രീൻ പകർത്താൻ ആവശ്യമായ GStreamer പ്ലഗിൻ ലഭ്യമല്ല ({})"),
("The screen sharing request timed out on the remote device", "വിദൂര ഉപകരണത്തി സ്ക്രീൻ പങ്കിടൽ അഭ്യർത്ഥനയുടെ സമയം കഴിഞ്ഞു"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk-ന് വിദൂര ഉപകരണത്തിലെ ഡെസ്ക്ടോപ്പ് സെഷനിലേക്ക് എത്താൻ കഴിയുന്നില്ല, സെഷൻ പ്രവർത്തിക്കുന്നുണ്ടെന്നും RustDesk-ന് അത് ഉപയോഗിക്കാമെന്നും ഉറപ്പാക്കുക"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "വിദൂര ഉപകരണത്തിലെ ഡെസ്ക്ടോപ്പ് പോർട്ടലിന് സ്ക്രീൻ പങ്കിടലിനോ വിദൂര നിയന്ത്രണത്തിനോ ആവശ്യമായ ശേഷിയില്ല, അതിന്റെ ബാക്കെൻഡ് ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ലായിരിക്കാം"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "വിദൂര ഉപകരണത്തിൽ സ്ക്രീൻ പങ്കിടൽ അനുവദിച്ചു, പക്ഷേ PipeWire കണക്ഷൻ തുറക്കാനായില്ല"),
("The screen sharing request ended without completing on the remote device", "വിദൂര ഉപകരണത്തിലെ സ്ക്രീൻ പങ്കിടൽ അഭ്യർത്ഥന പൂർത്തിയാകാതെ അവസാനിച്ചു"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "XDG Desktop Portal-ൽ നിന്ന് ഉപയോഗയോഗ്യമായ സ്ക്രീൻ RustDesk-ന് ലഭിച്ചില്ല, PipeWire ലൈബ്രറി വളരെ പഴയതാകാം"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "സ്ക്രീൻ പകർത്താൻ ആവശ്യമായ GStreamer ഘടകം RustDesk-ന് ലോഡ് ചെയ്യാനായില്ല ({})"),
("Relay fallback delay in seconds", "റിലേയിലേക്ക് മാറുന്നതിനുള്ള കാലതാമസം സെക്കൻഡിൽ"),
("relay-fallback-delay-tip", "ഇതിനകം സ്ഥാപിതമായ റിലേ കണക്ഷൻ നേരിട്ടുള്ള WebRTC കണക്ഷനായി എത്ര നേരം കാത്തിരിക്കുന്നു, അതിനുശേഷം അതിനുപകരം ഉപയോഗിക്കുന്നു. മന്ദഗതിയിലുള്ള നേരിട്ടുള്ള കണക്ഷന് കൂടുതൽ സമയം നൽകാൻ വർദ്ധിപ്പിക്കുക; നേരിട്ടുള്ള കണക്ഷൻ സാധ്യമല്ലാത്ത നെറ്റ്‌വർക്കുകളിൽ വേഗത്തിൽ റിലേയിലേക്ക് മാറാൻ കുറയ്ക്കുക. സ്ഥിരസ്ഥിതിയായ 2.5 സെക്കൻഡിനായി ശൂന്യമാക്കിയിടുക."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Aktiver WebRTC P2P-tilkobling"),
("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"),
("No one responded to the screen sharing request on the remote device", "Ingen svarte på forespørselen om skjermdeling på den eksterne enheten"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal avsluttet forespørselen om skjermdeling ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Et GStreamer-tillegg som kreves for skjermopptak mangler ({})"),
("The screen sharing request timed out on the remote device", "Forespørselen om skjermdeling fikk tidsavbrudd 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Forespørselen om skjermdeling på den eksterne enheten ble avsluttet uten å bli fullført"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk fikk ingen brukbar skjerm fra XDG Desktop Portal, PipeWire-biblioteket kan være for gammelt"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kunne ikke laste en GStreamer-komponent som kreves for skjermopptak ({})"),
("Relay fallback delay in seconds", "Forsinkelse før bruk av relé i sekunder"),
("relay-fallback-delay-tip", "Hvor lenge en allerede opprettet reléforbindelse venter på den direkte WebRTC-forbindelsen før den brukes i stedet. Øk verdien for å gi en treg direkteforbindelse mer tid; senk den for å gå raskere over til reléet på nettverk der direkte forbindelse ikke er mulig. La stå tom for standardverdien på 2.5 sekunder."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P-verbinding 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"),
("No one responded to the screen sharing request on the remote device", "Niemand heeft gereageerd op het verzoek om schermdeling op het externe apparaat"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal heeft het verzoek om schermdeling beëindigd ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Een GStreamer-plug-in die nodig is voor schermopname ontbreekt ({})"),
("The screen sharing request timed out on the remote device", "Het verzoek om schermdeling is verlopen 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Het verzoek om schermdeling op het externe apparaat is geëindigd zonder te zijn voltooid"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk kon geen bruikbaar scherm verkrijgen van de XDG Desktop Portal, de PipeWire-bibliotheek is mogelijk te oud"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kon een GStreamer-component die nodig is voor schermopname niet laden ({})"),
("Relay fallback delay in seconds", "Vertraging voordat relay wordt gebruikt in seconden"),
("relay-fallback-delay-tip", "Hoe lang een al tot stand gekomen relayverbinding wacht op de directe WebRTC-verbinding voordat deze in plaats daarvan wordt gebruikt. Verhoog de waarde om een trage directe verbinding meer tijd te geven; verlaag deze om op netwerken waar een directe verbinding niet mogelijk is sneller op de relay terug te vallen. Laat leeg voor de standaardwaarde van 2.5 seconden."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Włącz połączenie P2P WebRTC"),
("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"),
("No one responded to the screen sharing request on the remote device", "Nikt nie odpowiedział na żądanie udostępnienia ekranu na urządzeniu zdalnym"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal zakończył żądanie udostępnienia ekranu ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Brak wtyczki GStreamer wymaganej do przechwytywania ekranu ({})"),
("The screen sharing request timed out on the remote device", "Upłynął limit czasu żądania 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Żądanie udostępnienia ekranu na urządzeniu zdalnym zakończyło się bez ukończenia"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nie uzyskał użytecznego ekranu z XDG Desktop Portal, biblioteka PipeWire może być zbyt stara"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nie mógł załadować składnika GStreamer wymaganego do przechwytywania ekranu ({})"),
("Relay fallback delay in seconds", "Opóźnienie przed przejściem na przekaźnik w sekundach"),
("relay-fallback-delay-tip", "Jak długo nawiązane już połączenie przez przekaźnik czeka na bezpośrednie połączenie WebRTC, zanim zostanie użyte zamiast niego. Zwiększ, aby dać wolnemu połączeniu bezpośredniemu więcej czasu; zmniejsz, aby w sieciach, w których połączenie bezpośrednie jest niemożliwe, szybciej przechodzić na przekaźnik. Pozostaw puste, aby użyć wartości domyślnej 2.5 sekundy."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Ativar ligação P2P por WebRTC"),
("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"),
("No one responded to the screen sharing request on the remote device", "Ninguém respondeu ao pedido de partilha de ecrã no dispositivo remoto"),
("The XDG Desktop Portal ended the screen sharing request ({})", "O XDG Desktop Portal terminou o pedido de partilha de ecrã ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Falta um plugin do GStreamer necessário para capturar o ecrã ({})"),
("The screen sharing request timed out on the remote device", "O pedido de partilha de ecrã expirou no dispositivo remoto"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "O RustDesk não consegue 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "O pedido de partilha de ecrã no dispositivo remoto terminou sem ser concluído"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "O RustDesk não conseguiu obter um ecrã utilizável do XDG Desktop Portal, a biblioteca PipeWire pode ser demasiado antiga"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "O RustDesk não conseguiu carregar um componente do GStreamer necessário para capturar o ecrã ({})"),
("Relay fallback delay in seconds", "Atraso antes de recorrer ao retransmissor em segundos"),
("relay-fallback-delay-tip", "Quanto tempo uma ligação de retransmissão já estabelecida aguarda pela ligação direta WebRTC antes de ser usada em vez dela. Aumente para dar mais tempo a uma ligação direta lenta; diminua para recorrer mais cedo ao retransmissor em redes onde não é possível uma ligação direta. Deixe vazio para o valor predefinido de 2.5 segundos."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Click to upgrade", "Iniciar atualização"),
("Configure", "Configurar"),
("config_acc", "Para controlar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Acessibilidade\"."),
("config_screen", "Para acessar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\"/"),
("config_screen", "Para acessar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\""),
("Installing ...", "Instalando ..."),
("Install", "Instalar"),
("Installation", "Instalação"),
@@ -282,7 +282,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_service_will_start_tip", "Habilitar a Captura de Tela irá automaticamente inicalizar o serviço, permitindo que outros dispositivos solicitem uma conexão deste dispositivo."),
("android_stop_service_tip", "Fechar o serviço irá automaticamente fechar todas as conexões estabelecidas."),
("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor atualize para o Android 10 ou superior."),
("android_start_service_tip", "Toque em [Iniciar serviço] ou habilite a permissão [Captura de tela] para iniciar o serviço de compartilhamento de tela."),
("android_start_service_tip", "Toque em [Iniciar Serviço] ou habilite a permissão [Captura de Tela] para iniciar o serviço de compartilhamento de tela."),
("android_permission_may_not_change_tip", "As permissões para conexões estabelecidas podem não ser alteradas instantaneamente até que seja reconectado."),
("Account", "Conta"),
("Overwrite", "Substituir"),
@@ -691,7 +691,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Incorrect username or password.", "Usuário ou senha incorretos"),
("The user is not an administrator.", "O usuário não é administrador"),
("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"),
("Supported only in the installed version.", "Funciona somente na versão instalada"),
("Supported only in the installed version.", "Suportado somente na versão instalada"),
("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"),
("Preparing for installation ...", "Preparando para instalação ..."),
("Show my cursor", "Mostrar meu cursor"),
@@ -763,17 +763,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Bloquear tela"),
("Sync clipboard between sessions", "Sincronizar área de transferência entre sessões"),
("sync-clipboard-between-sessions-tip", "Texto ou imagens copiados em uma sessão remota também são enviados para a área de transferência das suas outras sessões conectadas."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("terminal-clipboard-write-tip", "Aplicativos do terminal podem copiar para a área de transferência"),
("Allow terminal apps to copy to clipboard", "Permitir cópia do terminal para a área de transferência"),
("Enable", "Habilitar"),
("Reuse one connection for port forwarding", "Reutilizar uma conexão para encaminhamento de portas"),
("port-forward-mux-tip", "Levar todas as conexões de um encaminhamento de portas por uma única conexão com o outro computador, em vez de conectar e fazer login novamente para cada uma."),
("port-forward-mux-tip", "Levar todas as conexões de um encaminhamento de portas por uma única conexão com o outro computador, em vez de estabelecer uma nova conexão e fazer login novamente para cada uma."),
("Enable WebRTC P2P connection", "Habilitar conexão WebRTC P2P"),
("Enable TCP hole punching", "Habilitar TCP hole punching"),
("The screen sharing request was declined on the remote device", "A solicitação de compartilhamento de tela foi recusada no dispositivo remoto"),
("No one responded to the screen sharing request on the remote device", "Ninguém respondeu à solicitação de compartilhamento de tela no dispositivo remoto"),
("The XDG Desktop Portal ended the screen sharing request ({})", "O XDG Desktop Portal encerrou a solicitação de compartilhamento de tela ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Falta um plugin do GStreamer necessário para capturar a tela ({})"),
("The screen sharing request timed out on the remote device", "A solicitação de compartilhamento de tela expirou no dispositivo remoto"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "O RustDesk não consegue acessar a sessão da área de trabalho no dispositivo remoto, verifique se há uma sessão da área de trabalho em execução e se o RustDesk pode acessá-la."),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "O portal da área de trabalho no dispositivo remoto não possui um recurso necessário para o compartilhamento de tela ou controle remoto. O backend pode não estar instalado."),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "O compartilhamento de tela foi autorizado no dispositivo remoto, mas não foi possível abrir a conexão com o PipeWire."),
("The screen sharing request ended without completing on the remote device", "A solicitação de compartilhamento de tela no dispositivo remoto foi encerrada sem ser concluída."),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "O RustDesk não conseguiu obter uma tela utilizável do XDG Desktop Portal. A biblioteca do PipeWire pode estar desatualizada."),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "O RustDesk não conseguiu carregar um componente do GStreamer necessário para a captura de tela ({})."),
("Relay fallback delay in seconds", "Atraso antes de recorrer ao retransmissor em segundos"),
("relay-fallback-delay-tip", "Tempo que a conexão de retransmissão aguarda pela conexão direta WebRTC. Aumente para dar mais tempo a conexões lentas; diminua para usar o retransmissor mais cedo. Deixe vazio para usar o padrão de 2,5 segundos."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "Para iniciar uma chamada de voz, ative \"Captura de áudio\" na página \"Compartilhamento de tela\".")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Activează conexiunea P2P prin WebRTC"),
("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ță"),
("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ță"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal a încheiat cererea de partajare a ecranului ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Lipsește un plugin GStreamer necesar pentru capturarea ecranului ({})"),
("The screen sharing request timed out on the remote device", "Cererea de partajare a ecranului a expirat 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 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"),
("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ă"),
("The screen sharing request ended without completing on the remote device", "Cererea de partajare a ecranului pe dispozitivul de la distanță s-a încheiat fără a fi finalizată"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nu a putut obține un ecran utilizabil de la XDG Desktop Portal, biblioteca PipeWire poate fi prea veche"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nu a putut încărca o componentă GStreamer necesară pentru capturarea ecranului ({})"),
("Relay fallback delay in seconds", "Întârziere înainte de trecerea la releu în secunde"),
("relay-fallback-delay-tip", "Cât timp așteaptă o conexiune prin releu deja stabilită conexiunea directă WebRTC înainte de a fi folosită în locul ei. Măriți valoarea pentru a acorda mai mult timp unei conexiuni directe lente; micșorați-o pentru a trece mai repede la releu în rețelele în care o conexiune directă nu este posibilă. Lăsați gol pentru valoarea implicită de 2.5 secunde."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -730,7 +730,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("preset-password-in-use-tip", "Установленный пароль сейчас используется."),
("Enable privacy mode", "Использовать режим конфиденциальности"),
("allow-remote-toolbar-docking-any-edge", "Разрешать прикрепление удалённой панели инструментов к любому краю окна"),
("API Token", "API-токен"),
("API Token", "Токен API"),
("Deploy", "Развернуть"),
("Custom ID (optional)", "Пользовательский ID (необязательно)"),
("server_requires_deployment_tip", "Сервер требует явного развёртывания этого устройства. Развернуть сейчас?"),
@@ -748,32 +748,38 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show on the minimized toolbar", "Показывать на свёрнутой панели инструментов"),
("All monitors", "Все мониторы"),
("#{} monitor", "Монитор {}"),
("conn-e2ee-unavailable-tip", "Не удалось проверить сквозное шифрование.\nУдаленное устройство, возможно, еще настраивается. Повторите попытку позже.\nЕсли это повторяется, сервер может быть ненадежным.\nВсе равно продолжить?"),
("conn-e2ee-unavailable-tip", "Невозможно проверить сквозное шифрование.\nУдалённое устройство, возможно, ещё настраивается. Повторите попытку позже.\nЕсли это повторяется, сервер может быть ненадёжным.\nВсё равно продолжить?"),
("ID whitelisting", "Список разрешённых ID"),
("Use ID whitelisting", "Использовать белый список ID"),
("id_whitelist_tip", "Только ID из белого списка могут получить доступ к моему устройству."),
("id_whitelist_wildcard_tip", "Поддерживаются подстановочные знаки: '*' соответствует любому количеству символов, '?' — ровно одному символу"),
("id_whitelist_wildcard_tip", "Поддерживаются подстановочные знаки: \"*\" соответствует любому количеству символов, \"?\" — ровно одному символу"),
("Invalid ID", "Неправильный ID"),
("Your ID is blocked by the peer", "Ваш ID заблокирован удалённым устройством"),
("Your ip is blocked by the peer", "Ваш IP-адрес заблокирован удалённым устройством"),
("id_whitelist_caveat_tip", "ID сообщается подключающимся клиентом. Белый список уменьшает поверхность атаки и не заменяет пароль или 2FA"),
("whitelist_cidr_tip", "Поддерживается нотация CIDR, например 192.168.1.0/24"),
("whitelist_cidr_tip", "Поддерживается нотация CIDR, например: 192.168.1.0/24"),
("Continue", "Продолжить"),
("Browser didn't open? Use the url below to sign in.", "Браузер не открылся? Используйте ссылку ниже для входа."),
("Lock canvas", "Заблокировать холст"),
("Sync clipboard between sessions", "Синхронизировать буфер обмена между сеансами"),
("sync-clipboard-between-sessions-tip", "Текст или изображения, скопированные в одном удалённом сеансе, также отправляются в буфер обмена других подключённых сеансов."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("terminal-clipboard-write-tip", "Приложение в терминале хочет скопировать текст в буфер обмена этого устройства. Если соответствующее разрешение предоставлено, оно применяется к приложениям в терминале во всех соединениях, пока вы не отключите его в настройках. Ручные копирование и вставка не затрагиваются."),
("Allow terminal apps to copy to clipboard", "Разрешить приложениям в терминале копирование в буфер обмена"),
("Enable", "Включить"),
("Reuse one connection for port forwarding", "Использовать одно подключение для перенаправления портов"),
("port-forward-mux-tip", "Передавать все соединения одного перенаправления портов через одно подключение к удалённому устройству вместо повторного подключения и входа для каждого из них."),
("Enable WebRTC P2P connection", "Использовать подключение WebRTC P2P"),
("Enable TCP hole punching", "Использовать TCP hole punching"),
("The screen sharing request was declined on the remote device", "Запрос на демонстрацию экрана отклонён на удалённом устройстве"),
("No one responded to the screen sharing request on the remote device", "Никто не ответил на запрос демонстрации экрана на удалённом устройстве"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal завершил запрос на демонстрацию экрана ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal не вернул экран для захвата, библиотека PipeWire может быть слишком старой"),
("A GStreamer plugin needed for screen capture is missing ({})", "Отсутствует плагин GStreamer, необходимый для захвата экрана ({})"),
("The screen sharing request timed out on the remote device", "Истекло время ожидания запроса на демонстрацию экрана на удалённом устройстве"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk не может получить доступ к сеансу рабочего стола на удалённом устройстве, проверьте, что сеанс запущен и доступен RustDesk"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "Порталу рабочего стола на удалённом устройстве не хватает возможности, необходимой для демонстрации экрана или удалённого управления, его реализация может быть не установлена"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "Демонстрация экрана была разрешена на удалённом устройстве, но не удалось открыть соединение PipeWire"),
("The screen sharing request ended without completing on the remote device", "Запрос на демонстрацию экрана на удалённом устройстве завершился, не будучи выполненным"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk не смог получить пригодный экран от XDG Desktop Portal, библиотека PipeWire может быть слишком старой"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не удалось загрузить компонент GStreamer, необходимый для захвата экрана ({})"),
("Relay fallback delay in seconds", "Задержка перед переходом на ретранслятор в секундах"),
("relay-fallback-delay-tip", "Сколько времени уже установленное соединение через ретранслятор ждёт прямое соединение WebRTC, прежде чем будет использовано вместо него. Увеличьте, чтобы дать медленному прямому соединению больше времени; уменьшите, чтобы быстрее переходить на ретранслятор в сетях, где прямое соединение невозможно. Оставьте пустым для значения по умолчанию 2.5 секунды."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "Чтобы начать голосовой вызов, включите \"Захват аудио\" на странице \"Демонстрация экрана\" настроек.")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Abìlita connessione P2P WebRTC"),
("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"),
("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"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal at acabadu sa rechesta de cumpartzidura de sa schermada ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Mancat unu plugin de GStreamer netzessàriu pro registrare sa schermada ({})"),
("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"),
("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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Sa rechesta de cumpartzidura de sa schermada in su dispositivu remotu est acabada chene si cumpletare"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk no at pòdidu otènnere una schermada impreabile dae XDG Desktop Portal, sa libreria PipeWire podet èssere tropu betza"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk no at pòdidu carrigare unu cumponente de GStreamer netzessàriu pro registrare sa schermada ({})"),
("Relay fallback delay in seconds", "Tardu prima de impreare su relè in segundos"),
("relay-fallback-delay-tip", "Cantu tempus una connessione de relè giai istabilida abetat sa connessione direta WebRTC prima de èssere impreada in su postu suo. Aumenta pro dare prus tempus a una connessione direta lenta; diminuì pro colare prima a su relè in sas retes in ue non si podet fàghere una connessione direta. Lassa bòidu pro su valore predefinidu de 2.5 segundos."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Povoliť pripojenie WebRTC P2P"),
("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á"),
("No one responded to the screen sharing request on the remote device", "Na žiadosť o zdieľanie obrazovky na vzdialenom zariadení nikto neodpovedal"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal ukončil žiadosť o zdieľanie obrazovky ({})"),
("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á"),
("A GStreamer plugin needed for screen capture is missing ({})", "Chýba zásuvný modul GStreamer potrebný na zachytenie obrazovky ({})"),
("The screen sharing request timed out on the remote device", "Vypršal časový limit žiadosti o zdieľanie obrazovky na vzdialenom zariadení"),
("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 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á"),
("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ť"),
("The screen sharing request ended without completing on the remote device", "Žiadosť o zdieľanie obrazovky na vzdialenom zariadení sa skončila bez dokončenia"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nezískal z XDG Desktop Portal použiteľnú obrazovku, knižnica PipeWire môže byť príliš stará"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nedokázal načítať komponent GStreamera potrebný na zachytenie obrazovky ({})"),
("Relay fallback delay in seconds", "Oneskorenie pred prepnutím na prenos v sekundách"),
("relay-fallback-delay-tip", "Ako dlho už nadviazané spojenie cez prenos čaká na priame spojenie WebRTC, kým sa použije namiesto neho. Zvýšte, aby pomalé priame spojenie malo viac času; znížte, aby sa v sieťach, kde priame spojenie nie je možné, skôr prešlo na prenos. Nechajte prázdne pre predvolenú hodnotu 2.5 sekundy."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Omogoči povezavo WebRTC P2P"),
("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"),
("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"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal je končal zahtevo za skupno rabo zaslona ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Manjka vtičnik GStreamer, potreben za zajem zaslona ({})"),
("The screen sharing request timed out on the remote device", "Zahteva za skupno rabo zaslona je na oddaljeni napravi potekla"),
("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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Zahteva za skupno rabo zaslona na oddaljeni napravi se je končala, ne da bi bila dokončana"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk od XDG Desktop Portala ni dobil uporabnega zaslona, knjižnica PipeWire je morda prestara"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ni mogel naložiti komponente GStreamer, potrebne za zajem zaslona ({})"),
("Relay fallback delay in seconds", "Zakasnitev pred preklopom na posrednika v sekundah"),
("relay-fallback-delay-tip", "Kako dolgo že vzpostavljena posredniška povezava čaka na neposredno povezavo WebRTC, preden se uporabi namesto nje. Povečajte, da počasni neposredni povezavi date več časa; zmanjšajte, da v omrežjih, kjer neposredna povezava ni mogoča, hitreje preklopite na posrednika. Pustite prazno za privzeto vrednost 2.5 sekunde."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -282,7 +282,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_service_will_start_tip", "Aktivizimi i \"Regjistrimi i ekranit\" do të nisë automatikisht shërbimin, duke lejuar pajisjet e tjera të kërkojnë një lidhje me pajisjen tuaj."),
("android_stop_service_tip", "Mbyllja e shërbimit do të mbyllë automatikisht të gjitha lidhjet e vendosura."),
("android_version_audio_tip", "Versioni aktual i Android nuk mbështet regjistrimin e audios, ju lutemi përmirësoni në Android 10 ose më të lartë."),
("android_start_service_tip", "Trokitni te [Nis shërbimin] ose aktivizoni lejen [Kapja e ekranit] për të nisur shërbimin e ndarjes së ekranit."),
("android_start_service_tip", "Trokitni te [Nis Shërbimin] ose aktivizoni lejen [Kapja e ekranit] për të nisur shërbimin e ndarjes së ekranit."),
("android_permission_may_not_change_tip", "Lejet për lidhjet e themeluara mund të mos ndryshohen menjëherë derisa të rilidheni."),
("Account", "Llogaria"),
("Overwrite", "Përshkruaj"),
@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Aktivizo lidhjen WebRTC P2P"),
("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"),
("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"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal e përfundoi kërkesën për ndarjen e ekranit ({})"),
("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"),
("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 timed out on the remote device", "Kërkesa për ndarjen e ekranit skadoi 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Kërkesa për ndarjen e ekranit në pajisjen e largët përfundoi pa u kryer"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nuk mori një ekran të përdorshëm nga XDG Desktop Portal, biblioteka PipeWire mund të jetë shumë e vjetër"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nuk mundi të ngarkojë një komponent të GStreamer të nevojshëm për regjistrimin e ekranit ({})"),
("Relay fallback delay in seconds", "Vonesa para kalimit te releja në sekonda"),
("relay-fallback-delay-tip", "Sa gjatë pret një lidhje releje tashmë e vendosur lidhjen e drejtpërdrejtë WebRTC përpara se të përdoret në vend të saj. Rriteni për t'i dhënë më shumë kohë një lidhjeje të drejtpërdrejtë të ngadaltë; uleni për të kaluar më shpejt te releja në rrjete ku lidhja e drejtpërdrejtë nuk është e mundur. Lëreni bosh për vlerën e parazgjedhur prej 2.5 sekondash."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Omogući WebRTC P2P konekciju"),
("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"),
("No one responded to the screen sharing request on the remote device", "Niko nije odgovorio na zahtev za deljenje ekrana na udaljenom uređaju"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal je završio zahtev za deljenje ekrana ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Nedostaje GStreamer dodatak potreban za snimanje ekrana ({})"),
("The screen sharing request timed out on the remote device", "Zahtev za deljenje ekrana je istekao 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Zahtev za deljenje ekrana na udaljenom uređaju završio se bez dovršetka"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk nije mogao da dobije upotrebljiv ekran od XDG Desktop Portala, PipeWire biblioteka je možda prestara"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nije mogao da učita GStreamer komponentu potrebnu za snimanje ekrana ({})"),
("Relay fallback delay in seconds", "Кашњење пре преласка на релеј у секундама"),
("relay-fallback-delay-tip", "Колико дуго већ успостављена релејна веза чека на директну WebRTC везу пре него што се употреби уместо ње. Повећајте да бисте спорој директној вези дали више времена; смањите да бисте на мрежама где директна веза није могућа брже прешли на релеј. Оставите празно за подразумевану вредност од 2.5 секунде."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -279,7 +279,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_input_permission_tip1", "Android rättigheter saknas"),
("android_input_permission_tip2", "Gå till systeminställningarna, hitta [Installed Services], sätt på [RustDesk Input] tjänsten."),
("android_new_connection_tip", "Ny kontrollförfrågan mottagen, denna vill kontrollera din enhet."),
("android_service_will_start_tip", "Sätter du på \"skärminspelning\" kommer tjänsten automatiskt att starta. Detta tillåter andra enheter att kontrollera din enhet."),
("android_service_will_start_tip", "Sätter du på \"Skärminspelning\" kommer tjänsten automatiskt att starta. Detta tillåter andra enheter att kontrollera din enhet."),
("android_stop_service_tip", "Genom att stänga av tjänsten kommer alla enheter att kopplas ifrån."),
("android_version_audio_tip", "Din version av Android stödjer inte ljudinspelning, Android 10 eller nyare krävs"),
("android_start_service_tip", "android_start_service_tips"),
@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "Aktivera WebRTC P2P anslutning"),
("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"),
("No one responded to the screen sharing request on the remote device", "Ingen svarade på begäran om skärmdelning på fjärrenheten"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal avslutade begäran om skärmdelning ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "En GStreamer-insticksmodul som krävs för skärminspelning saknas ({})"),
("The screen sharing request timed out on the remote device", "Begäran om skärmdelning nådde tidsgränsen 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 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"),
("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"),
("The screen sharing request ended without completing on the remote device", "Begäran om skärmdelning på fjärrenheten avslutades utan att slutföras"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk fick ingen användbar skärm från XDG Desktop Portal, PipeWire-biblioteket kan vara för gammalt"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kunde inte läsa in en GStreamer-komponent som krävs för skärminspelning ({})"),
("Relay fallback delay in seconds", "Fördröjning innan relä används i sekunder"),
("relay-fallback-delay-tip", "Hur länge en redan upprättad reläanslutning väntar på den direkta WebRTC-anslutningen innan den används i stället. Öka värdet för att ge en långsam direktanslutning mer tid; sänk det för att snabbare falla tillbaka på reläet i nätverk där direktanslutning inte är möjlig. Lämna tomt för standardvärdet 2.5 sekunder."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P இணைப்பு இயக்கு"),
("Enable TCP hole punching", "TCP hole punching இயக்கு"),
("The screen sharing request was declined on the remote device", "தொலைநிலை சாதனத்தில் திரை பகிர்வு கோரிக்கை நிராகரிக்கப்பட்டது"),
("No one responded to the screen sharing request on the remote device", "தொலைநிலை சாதனத்தில் திரை பகிர்வு கோரிக்கைக்கு யாரும் பதிலளிக்கவில்லை"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal திரை பகிர்வு கோரிக்கையை முடித்தது ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal பதிவு செய்ய எந்தத் திரையையும் வழங்கவில்லை, PipeWire நூலகம் மிகவும் பழையதாக இருக்கலாம்"),
("A GStreamer plugin needed for screen capture is missing ({})", "திரைப் பதிவுக்குத் தேவையான GStreamer செருகுநிரல் இல்லை ({})"),
("The screen sharing request timed out on the remote device", "தொலைநிலை சாதனத்தில் திரை பகிர்வு கோரிக்கையின் நேரம் முடிந்தது"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk தொலைநிலை சாதனத்தின் டெஸ்க்டாப் அமர்வை அணுக முடியவில்லை, ஒரு அமர்வு இயங்குகிறதா என்பதையும் RustDesk அதைப் பயன்படுத்த முடியுமா என்பதையும் சரிபார்க்கவும்"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "தொலைநிலை சாதனத்தின் டெஸ்க்டாப் போர்ட்டலில் திரை பகிர்வுக்கோ தொலை கட்டுப்பாட்டுக்கோ தேவையான திறன் இல்லை, அதன் பின்தளம் நிறுவப்படாமல் இருக்கலாம்"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "தொலைநிலை சாதனத்தில் திரை பகிர்வு அனுமதிக்கப்பட்டது, ஆனால் PipeWire இணைப்பைத் திறக்க முடியவில்லை"),
("The screen sharing request ended without completing on the remote device", "தொலைநிலை சாதனத்தில் திரை பகிர்வு கோரிக்கை நிறைவடையாமல் முடிந்தது"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "XDG Desktop Portal-லிருந்து பயன்படுத்தக்கூடிய திரையை RustDesk பெற முடியவில்லை, PipeWire நூலகம் மிகவும் பழையதாக இருக்கலாம்"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "திரைப் பதிவுக்குத் தேவையான GStreamer கூறை RustDesk ஏற்ற முடியவில்லை ({})"),
("Relay fallback delay in seconds", "ரிலேக்கு மாறுவதற்கான தாமதம் வினாடிகளில்"),
("relay-fallback-delay-tip", "ஏற்கனவே நிறுவப்பட்ட ரிலே இணைப்பு நேரடி WebRTC இணைப்புக்காக எவ்வளவு நேரம் காத்திருக்கிறது, அதன் பிறகு அதற்குப் பதிலாகப் பயன்படுத்தப்படுகிறது. மெதுவான நேரடி இணைப்புக்கு அதிக நேரம் வழங்க அதிகரிக்கவும்; நேரடி இணைப்பு சாத்தியமில்லாத பிணையங்களில் விரைவாக ரிலேக்கு மாற குறைக்கவும். இயல்புநிலை 2.5 வினாடிகளுக்கு காலியாக விடவும்."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", ""),
("Enable TCP hole punching", ""),
("The screen sharing request was declined on the remote device", ""),
("No one responded to the screen sharing request on the remote device", ""),
("The XDG Desktop Portal ended the screen sharing request ({})", ""),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", ""),
("A GStreamer plugin needed for screen capture is missing ({})", ""),
("The screen sharing request timed out on the remote device", ""),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", ""),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", ""),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", ""),
("The screen sharing request ended without completing on the remote device", ""),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", ""),
("RustDesk could not load a GStreamer component needed for screen capture ({})", ""),
("Relay fallback delay in seconds", ""),
("relay-fallback-delay-tip", ""),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "เปิดใช้งานการเชื่อมต่อ P2P แบบ WebRTC"),
("Enable TCP hole punching", "เปิดใช้งาน TCP hole punching"),
("The screen sharing request was declined on the remote device", "คำขอแชร์หน้าจอถูกปฏิเสธบนอุปกรณ์ระยะไกล"),
("No one responded to the screen sharing request on the remote device", "ไม่มีผู้ใดตอบรับคำขอแชร์หน้าจอบนอุปกรณ์ระยะไกล"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal ได้ยุติคำขอแชร์หน้าจอ ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal ไม่ได้ส่งคืนหน้าจอสำหรับการบันทึก ไลบรารี PipeWire อาจเก่าเกินไป"),
("A GStreamer plugin needed for screen capture is missing ({})", "ไม่พบปลั๊กอิน GStreamer ที่จำเป็นสำหรับการบันทึกหน้าจอ ({})"),
("The screen sharing request timed out on the remote device", "คำขอแชร์หน้าจอบนอุปกรณ์ระยะไกลหมดเวลา"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk ไม่สามารถเข้าถึงเซสชันเดสก์ท็อปบนอุปกรณ์ระยะไกล ตรวจสอบว่าเซสชันเดสก์ท็อปกำลังทำงานและ RustDesk ใช้งานได้"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "พอร์ทัลเดสก์ท็อปบนอุปกรณ์ระยะไกลขาดความสามารถที่จำเป็นสำหรับการแชร์หน้าจอหรือการควบคุมระยะไกล แบ็กเอนด์ของมันอาจยังไม่ได้ติดตั้ง"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "การแชร์หน้าจอได้รับอนุญาตบนอุปกรณ์ระยะไกลแล้ว แต่ไม่สามารถเปิดการเชื่อมต่อ PipeWire ได้"),
("The screen sharing request ended without completing on the remote device", "คำขอแชร์หน้าจอบนอุปกรณ์ระยะไกลสิ้นสุดลงโดยไม่เสร็จสมบูรณ์"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk ไม่สามารถรับหน้าจอที่ใช้งานได้จาก XDG Desktop Portal ไลบรารี PipeWire อาจเก่าเกินไป"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ไม่สามารถโหลดส่วนประกอบ GStreamer ที่จำเป็นสำหรับการบันทึกหน้าจอได้ ({})"),
("Relay fallback delay in seconds", "เวลารอก่อนเปลี่ยนไปใช้รีเลย์ (วินาที)"),
("relay-fallback-delay-tip", "การเชื่อมต่อผ่านรีเลย์ที่สร้างไว้แล้วจะรอการเชื่อมต่อ WebRTC โดยตรงนานเท่าใดก่อนที่จะถูกใช้แทน เพิ่มค่าเพื่อให้การเชื่อมต่อโดยตรงที่ช้ามีเวลามากขึ้น ลดค่าเพื่อเปลี่ยนไปใช้รีเลย์เร็วขึ้นในเครือข่ายที่ไม่สามารถเชื่อมต่อโดยตรงได้ เว้นว่างไว้เพื่อใช้ค่าเริ่มต้น 2.5 วินาที"),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -441,7 +441,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Voice call", "Sesli görüşme"),
("Text chat", "Metin sohbeti"),
("Stop voice call", "Sesli görüşmeyi durdur"),
("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; aktarmalı bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde aktarma sunucusu kullanmak istiyorsanız ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her Zaman Aktarmalı Üzerinden Bağlan\" seçeneğini seçebilirsiniz."),
("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; aktarmalı bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde aktarma sunucusu kullanmak istiyorsanız ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her zaman aktarmalı üzerinden bağlan\" seçeneğini seçebilirsiniz."),
("Reconnect", "Yeniden Bağlan"),
("Codec", "Kodlayıcı"),
("Resolution", "Çözünürlük"),
@@ -771,9 +771,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable WebRTC P2P connection", "WebRTC P2P bağlantısını 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"),
("No one responded to the screen sharing request on the remote device", "Uzak cihazdaki ekran paylaşımı isteğine kimse yanıt vermedi"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal ekran paylaşımı isteğini sonlandırdı ({})"),
("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"),
("A GStreamer plugin needed for screen capture is missing ({})", "Ekran yakalama için gereken GStreamer eklentisi eksik ({})"),
("The screen sharing request timed out on the remote device", "Uzak cihazdaki ekran paylaşımı isteği zaman aşımına uğradı"),
("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 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"),
("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ı"),
("The screen sharing request ended without completing on the remote device", "Uzak cihazdaki ekran paylaşımı isteği tamamlanmadan sona erdi"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk, XDG Desktop Portal'dan kullanılabilir bir ekran alamadı, PipeWire kitaplığı çok eski olabilir"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ekran yakalama için gereken GStreamer bileşenini yükleyemedi ({})"),
("Relay fallback delay in seconds", "Aktarıcıya geçiş gecikmesi (saniye)"),
("relay-fallback-delay-tip", "Zaten kurulmuş bir aktarıcı bağlantısının, onun yerine kullanılmadan önce doğrudan WebRTC bağlantısını ne kadar beklediğidir. Yavaş bir doğrudan bağlantıya daha fazla süre tanımak için artırın; doğrudan bağlantının kurulamadığı ağlarda aktarıcıya daha erken geçmek için azaltın. Varsayılan 2.5 saniye için boş bırakın."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -770,10 +770,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("port-forward-mux-tip", "同一條連接埠轉送規則上的所有連線共用一條到對方的連線,而不是每條連線都重新連線並登入一次。"),
("Enable WebRTC P2P connection", "啟用 WebRTC P2P 連線"),
("Enable TCP hole punching", "啟用 TCP 打洞"),
("The screen sharing request was declined on the remote device", "遠端裝置上拒絕了螢幕分享要求"),
("No one responded to the screen sharing request on the remote device", "遠端裝置上無人回應螢幕分享要求"),
("The XDG Desktop Portal ended the screen sharing request ({})", "XDG Desktop Portal 結束了螢幕分享要求 ({})"),
("The XDG Desktop Portal returned no screen to capture, the PipeWire library may be too old", "XDG Desktop Portal 未傳回可擷取的螢幕PipeWire 函式庫可能過舊"),
("A GStreamer plugin needed for screen capture is missing ({})", "缺少螢幕擷取所需的 GStreamer 外掛程式 ({})"),
("The screen sharing request was declined on the remote device", "遠端裝置上的使用者拒絕了螢幕分享要求"),
("The screen sharing request timed out on the remote device", "遠端裝置上螢幕分享要求逾時了"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "RustDesk 無法存取遠端裝置的桌面工作階段,請確認工作階段已啟動且 RustDesk 可以使用它"),
("The desktop portal on the remote device is missing a capability needed for screen sharing or remote control, its backend may not be installed", "遠端裝置上的桌面入口缺少螢幕分享或遠端控制所需的功能,可能沒有安裝它的後端"),
("Screen sharing was approved on the remote device, but the PipeWire connection could not be opened", "遠端裝置上已核准螢幕分享,但無法開啟 PipeWire 連線"),
("The screen sharing request ended without completing on the remote device", "遠端裝置上的螢幕分享要求已結束,但未完成"),
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "RustDesk 無法從 XDG Desktop Portal 取得可用的螢幕PipeWire 函式庫可能過舊"),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk 無法載入螢幕擷取所需的 GStreamer 元件 ({})"),
("Relay fallback delay in seconds", "回退到中繼前的等待時間(秒)"),
("relay-fallback-delay-tip", "已經建立的中繼連線會等待直連的 WebRTC 多久,超過這個時間就改用中繼。調大可以讓較慢的直連有更多機會勝出;調小則在無法直連的網路上更快回退到中繼。留空表示使用預設值 2.5 秒。"),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

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