Compare commits

..

26 Commits

Author SHA1 Message Date
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
106 changed files with 8868 additions and 693 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

21
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#69ad2578adc9200093fc81cdfbdad63dbc4274f9"
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",
]
@@ -4280,7 +4280,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 +6496,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",
@@ -7139,6 +7139,7 @@ dependencies = [
"lazy_static",
"libpulse-binding",
"libpulse-simple-binding",
"libsamplerate-sys",
"libxdo-sys",
"mac_address",
"magnum-opus",
@@ -9731,7 +9732,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=db3b07a9dd8f195916c89c2e62a8911402b11d27#db3b07a9dd8f195916c89c2e62a8911402b11d27"
dependencies = [
"arc-swap",
"async-trait",
@@ -9771,7 +9772,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=db3b07a9dd8f195916c89c2e62a8911402b11d27#db3b07a9dd8f195916c89c2e62a8911402b11d27"
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,8 @@ libxdo-sys = { path = "libs/libxdo-sys-stub" }
# the SACK settle the rest (F-RTO), timed from the latest send, so a stall no longer resends the
# 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-util = { git = "https://github.com/rustdesk-org/webrtc", rev = "db3b07a9dd8f195916c89c2e62a8911402b11d27" }
webrtc-sctp = { git = "https://github.com/rustdesk-org/webrtc", rev = "db3b07a9dd8f195916c89c2e62a8911402b11d27" }
[package.metadata.winres]
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."

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 @@
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

@@ -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

@@ -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

@@ -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,7 +521,7 @@ 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())
}
// The name alone is usually the generic `org.freedesktop.DBus.Error.Failed`; the message is
@@ -526,7 +534,12 @@ fn dbus_stage_err(stage: &str, err: &dbus::Error) -> String {
(Some(name), _) if !name.is_empty() => name.to_owned(),
(_, message) => message.unwrap_or_default().to_owned(),
};
stage_err(stage, "dbus", &detail)
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)]
@@ -605,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());
@@ -612,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
})
@@ -912,6 +936,7 @@ fn on_create_session_response(
});
}
trace.waiting(PortalStage::SelectSources);
handle_response(
c,
get_request_path(c, select_sources_handle_token)?,
@@ -925,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
@@ -938,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)?,
@@ -951,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(())
@@ -985,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)?,
@@ -998,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(())
}
@@ -1024,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)?,
@@ -1031,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(())
@@ -1051,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,
@@ -1078,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(())
}

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,10 @@ use crate::ui_session_interface::SessionPermissionConfig;
pub use super::lang::*;
#[cfg(not(target_os = "linux"))]
mod audio_playback;
#[cfg(all(test, not(target_os = "linux")))]
mod audio_state_tests;
pub mod file_trait;
pub mod helper;
pub mod io_loop;
@@ -569,7 +573,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 +614,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 +1134,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 +1462,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 +1480,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 +2073,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 +2082,55 @@ 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(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 +2138,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 +2150,7 @@ impl Default for AudioBuffer {
)),
48000 * 2,
[0; 30],
Arc::new(std::sync::atomic::AtomicUsize::new(0)),
)
}
}
@@ -2153,27 +2225,36 @@ 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 {
log::debug!(
"Audio buffer capacity discard: samples={discarded}, generation={generation}"
);
}
occupied
}
/// append pcm to audio buffer, trying to drop data
@@ -2185,6 +2266,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<()> {
@@ -2238,6 +2354,9 @@ impl AudioHandler {
}
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,6 +2381,7 @@ impl AudioHandler {
} else {
build_output_stream(config)?;
}
self.audio_resampler = audio_resampler;
Ok(())
}
@@ -2274,10 +2394,17 @@ impl AudioHandler {
}
match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) {
Ok(d) => {
#[cfg(target_os = "linux")]
let keep_existing_stream = self.simple.is_some()
&& self.sample_rate.0 == f.sample_rate
&& u32::from(self.channels) == f.channels;
#[cfg(not(target_os = "linux"))]
let keep_existing_stream = false;
let buffer = vec![0.; f.sample_rate as usize * f.channels as usize];
self.audio_decoder = Some((d, buffer));
self.channels = f.channels as _;
allow_err!(self.start_audio(f));
let result = self.start_audio(f);
self.handle_audio_start_result(result, keep_existing_stream);
}
Err(err) => {
log::error!("Failed to create audio decoder: {}", err);
@@ -2285,11 +2412,31 @@ 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")]
@@ -2298,39 +2445,40 @@ impl AudioHandler {
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));
}
});
}
@@ -2350,63 +2498,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 +2539,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`].

View File

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

View File

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

View File

@@ -0,0 +1,113 @@
use super::{create_audio_resampler, AudioDecoder, AudioFrame, AudioHandler, Stereo};
use cpal::traits::StreamTrait;
use hbb_common::anyhow::anyhow;
use magnum_opus::{Application::LowDelay, Encoder};
use ringbuf::{ring_buffer::RbBase, Rb};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
const INPUT_RATE: u32 = 24_000;
const OUTPUT_RATE: u32 = 48_000;
const CHANNELS: u16 = 2;
const PACKETS_PER_SECOND: usize = 100;
const MAX_PACKET_BYTES: usize = 4_096;
const SAMPLE_VALUE: f32 = 0.25;
struct TrackedAudioStream(Arc<AtomicBool>);
impl StreamTrait for TrackedAudioStream {
fn play(&self) -> Result<(), cpal::PlayStreamError> {
Ok(())
}
fn pause(&self) -> Result<(), cpal::PauseStreamError> {
Ok(())
}
}
impl Drop for TrackedAudioStream {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
fn decoder(sample_rate: u32) -> (AudioDecoder, Vec<f32>) {
(
AudioDecoder::new(sample_rate, Stereo).unwrap(),
vec![0.0; sample_rate as usize * CHANNELS as usize],
)
}
fn active_handler(input_rate: u32) -> (AudioHandler, Arc<AtomicBool>) {
let dropped = Arc::new(AtomicBool::new(false));
let handler = AudioHandler {
audio_decoder: Some(decoder(input_rate)),
audio_resampler: create_audio_resampler(input_rate, OUTPUT_RATE, CHANNELS).unwrap(),
sample_rate: (input_rate, OUTPUT_RATE),
audio_stream: Some(Box::new(TrackedAudioStream(dropped.clone()))),
channels: CHANNELS,
device_channel: CHANNELS,
..Default::default()
};
handler.playback_status.ready.store(true, Ordering::Release);
(handler, dropped)
}
fn audio_frame() -> AudioFrame {
let samples = OUTPUT_RATE as usize / PACKETS_PER_SECOND * CHANNELS as usize;
let mut encoder = Encoder::new(OUTPUT_RATE, Stereo, LowDelay).unwrap();
AudioFrame {
data: encoder
.encode_vec_float(&vec![SAMPLE_VALUE; samples], MAX_PACKET_BYTES)
.unwrap()
.into(),
..Default::default()
}
}
#[test]
fn failed_format_change_discards_old_playback_state() {
let (mut handler, dropped) = active_handler(INPUT_RATE);
handler
.audio_buffer
.0
.lock()
.unwrap()
.push_slice(&[SAMPLE_VALUE; CHANNELS as usize]);
handler.audio_decoder = Some(decoder(OUTPUT_RATE));
handler.sample_rate = (OUTPUT_RATE, OUTPUT_RATE);
handler.handle_audio_start_result(
Err(anyhow!("Injected output stream startup failure")),
false,
);
assert!(dropped.load(Ordering::SeqCst));
assert!(handler.audio_stream.is_none());
assert!(handler.audio_resampler.is_none());
assert!(handler.audio_decoder.is_none());
assert!(!handler.playback_status.ready.load(Ordering::Acquire));
handler.handle_frame(audio_frame());
assert_eq!(handler.audio_buffer.0.lock().unwrap().occupied_len(), 0);
}
#[test]
fn successful_start_or_compatible_failure_preserves_audio_packet_duration() {
for result in [
Ok(()),
Err(anyhow!("Injected compatible stream replacement failure")),
] {
let (mut handler, dropped) = active_handler(OUTPUT_RATE);
handler.handle_audio_start_result(result, true);
handler.handle_frame(audio_frame());
assert!(!dropped.load(Ordering::SeqCst));
assert_eq!(
handler.audio_buffer.0.lock().unwrap().occupied_len(),
OUTPUT_RATE as usize / PACKETS_PER_SECOND * CHANNELS as usize
);
}
}

View File

@@ -15,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.

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

@@ -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

@@ -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,14 @@ 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 ثانية."),
].iter().cloned().collect();
}

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

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

View File

@@ -771,9 +771,14 @@ 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 секунды."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 секунди."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 秒。"),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 δευτερολέπτων."),
].iter().cloned().collect();
}

View File

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

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 ثانیه خالی بگذارید."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 წამისთვის."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 સેકન્ડ માટે ખાલી રાખો."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 שניות."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 सेकंड के लिए खाली छोड़ें।"),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].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,16 @@ 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", ""),
("relay-fallback-delay-tip", ""),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 秒になります。"),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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초가 사용됩니다."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 секунд үшін бос қалдырыңыз."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 സെക്കൻഡിനായി ശൂന്യമാക്കിയിടുക."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -691,7 +691,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Incorrect username or password.", "Usuário ou senha incorretos"),
("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"),
@@ -767,13 +767,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Allow terminal apps to copy to clipboard", ""),
("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", "Quanto tempo uma conexão de retransmissão já estabelecida espera pela conexão direta WebRTC antes de ser usada no lugar dela. Aumente para dar mais tempo a uma conexão direta lenta; diminua para recorrer mais cedo ao retransmissor em redes onde não é possível uma conexão direta. Deixe vazio para o valor padrão de 2.5 segundos."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 секунды."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 секунде."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 வினாடிகளுக்கு காலியாக விடவும்."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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", ""),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 วินาที"),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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."),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 秒。"),
].iter().cloned().collect();
}

View File

@@ -771,9 +771,14 @@ 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 секунди."),
].iter().cloned().collect();
}

View File

@@ -771,10 +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 سیکنڈ کے لیے خالی چھوڑ دیں۔"),
].iter().cloned().collect();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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