Compare commits

...

10 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
81 changed files with 3255 additions and 272 deletions

View File

@@ -374,6 +374,12 @@ jobs:
- name: Add MSBuild to PATH - name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2 uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
with:
# Select the MSBuild process architecture; -p:Platform sets the MSI target.
# Native ARM64 tools give the compiler more address space for PCH files
# (C3859/C1076 were reported by HostX86\arm64\CL.exe).
# Keep the action's default x86 MSBuild for the existing x64 MSI job.
msbuild-architecture: ${{ matrix.job.arch == 'aarch64' && 'arm64' || 'x86' }}
- name: Build msi - name: Build msi
# Builds the MSI for the matrix arch. res/msi (WiX v4 + native CustomActions) carries # Builds the MSI for the matrix arch. res/msi (WiX v4 + native CustomActions) carries
@@ -492,11 +498,20 @@ jobs:
version: ${{ env.LLVM_VERSION }} version: ${{ env.LLVM_VERSION }}
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 shell: bash
with: run: |
toolchain: nightly-2023-10-13-${{ matrix.job.target }} # must use nightly here, because of abi_thiscall feature required # Sciter's abi_thiscall feature requires nightly Rust.
targets: ${{ matrix.job.target }} # Use an i686 host toolchain so build scripts can load the 32-bit LLVM installed above.
components: "rustfmt" # 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 - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with: with:

13
Cargo.lock generated
View File

@@ -985,9 +985,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.10.1" version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
dependencies = [ dependencies = [
"serde 1.0.228", "serde 1.0.228",
] ]
@@ -1791,9 +1791,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-epoch" name = "crossbeam-epoch"
version = "0.9.18" version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
@@ -6496,9 +6496,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn-proto" name = "quinn-proto"
version = "0.11.13" version = "0.11.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
dependencies = [ dependencies = [
"bytes", "bytes",
"getrandom 0.3.2", "getrandom 0.3.2",
@@ -7139,6 +7139,7 @@ dependencies = [
"lazy_static", "lazy_static",
"libpulse-binding", "libpulse-binding",
"libpulse-simple-binding", "libpulse-simple-binding",
"libsamplerate-sys",
"libxdo-sys", "libxdo-sys",
"mac_address", "mac_address",
"magnum-opus", "magnum-opus",

View File

@@ -22,7 +22,7 @@ path = "src/service.rs"
[features] [features]
inline = [] inline = []
use_samplerate = ["samplerate"] use_samplerate = ["samplerate", "libsamplerate-sys"]
use_rubato = ["rubato"] use_rubato = ["rubato"]
use_dasp = ["dasp"] use_dasp = ["dasp"]
flutter = ["flutter_rust_bridge"] 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 } dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
rubato = { version = "0.12", optional = true } rubato = { version = "0.12", optional = true }
samplerate = { version = "0.2", optional = true } samplerate = { version = "0.2", optional = true }
libsamplerate-sys = { version = "0.1.12", optional = true }
uuid = { version = "1.3", features = ["v4"] } uuid = { version = "1.3", features = ["v4"] }
num_cpus = "1.15" num_cpus = "1.15"
bytes = { version = "1.4", features = ["serde"] } bytes = { version = "1.4", features = ["serde"] }

View File

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

View File

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

View File

@@ -3601,9 +3601,11 @@ class QualityMonitorModel with ChangeNotifier {
bool get show => _show; bool get show => _show;
QualityMonitorData get data => _data; QualityMonitorData get data => _data;
// Only a WebRTC session names its transport here: web has no session tab // Only a WebRTC session on the web names its transport here: web has no
// to show it on, and WebRTC is the one path that can be direct or TURN. // 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 { String? get webrtcTransport {
if (!isWeb) return null;
final ffiModel = parent.target?.ffiModel; final ffiModel = parent.target?.ffiModel;
if (ffiModel == null) return null; if (ffiModel == null) return null;
final streamType = ffiModel.cachedPeerData.streamType; final streamType = ffiModel.cachedPeerData.streamType;

View File

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

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::*; pub use super::lang::*;
#[cfg(not(target_os = "linux"))]
mod audio_playback;
#[cfg(all(test, not(target_os = "linux")))]
mod audio_state_tests;
pub mod file_trait; pub mod file_trait;
pub mod helper; pub mod helper;
pub mod io_loop; pub mod io_loop;
@@ -569,7 +573,7 @@ impl Client {
return race_transports_prefer_webrtc( return race_transports_prefer_webrtc(
preferred_fut, preferred_fut,
vec![fallback_fut], vec![fallback_fut],
Self::WEBRTC_PREFER_WINDOW_MS, Self::relay_fallback_delay_ms(),
|result| result.0 .1, |result| result.0 .1,
) )
.await; .await;
@@ -610,11 +614,27 @@ impl Client {
/// ones that traverse NAT. /// ones that traverse NAT.
const MAX_PENDING_WEBRTC_ICE: usize = 64; const MAX_PENDING_WEBRTC_ICE: usize = 64;
/// Prefer-P2P window: how long a WebRTC attempt outranks an already-established relay /// Default relay fallback delay: how long an already-established relay result is held back
/// result, and the floor for a punch-path WebRTC attempt whose race timeout is tuned for a /// while a WebRTC attempt is still in flight, and the floor for a punch-path WebRTC attempt
/// raw TCP SYN. Long enough for candidate trickle + ICE checks + DTLS on high-latency /// whose race timeout is tuned for a raw TCP SYN. Long enough for candidate trickle + ICE
/// links; short enough that UDP-blocked networks settle on relay without a noticeable wait. /// checks + DTLS on high-latency links; short enough that UDP-blocked networks settle on
const WEBRTC_PREFER_WINDOW_MS: u64 = 2500; /// 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 /// UDP-NAT-test wait when the TCP clock is implausible (see TCP_RTT_PLAUSIBLE_MIN). The
/// normal bound is `rtt / 2`: the test has been running since before the TCP connect, so on /// normal bound is `rtt / 2`: the test has been running since before the TCP connect, so on
@@ -1114,7 +1134,7 @@ impl Client {
race_transports_prefer_webrtc( race_transports_prefer_webrtc(
webrtc_fut, webrtc_fut,
connect_futures, connect_futures,
Self::WEBRTC_PREFER_WINDOW_MS, Self::relay_fallback_delay_ms(),
|result| result.3, |result| result.3,
) )
.await .await
@@ -1442,7 +1462,7 @@ impl Client {
// so a viable P2P path is not abandoned before it can complete; TCP/UDP keep the // so a viable P2P path is not abandoned before it can complete; TCP/UDP keep the
// tighter timeout, so a working direct connection still wins immediately, and the // tighter timeout, so a working direct connection still wins immediately, and the
// relay fallback only waits the extra time when direct attempts all failed. // relay fallback only waits the extra time when direct attempts all failed.
let webrtc_timeout = connect_timeout.max(Self::WEBRTC_PREFER_WINDOW_MS); let webrtc_timeout = connect_timeout.max(Self::relay_fallback_delay_ms());
async move { async move {
raced.wait_connected(webrtc_timeout).await?; raced.wait_connected(webrtc_timeout).await?;
// Resolve the pair here: a TURN win is relayed, not direct, and must be held // Resolve the pair here: a TURN win is relayed, not direct, and must be held
@@ -1460,7 +1480,7 @@ impl Client {
race_transports_prefer_webrtc( race_transports_prefer_webrtc(
webrtc_fut, webrtc_fut,
direct_futures, direct_futures,
Self::WEBRTC_PREFER_WINDOW_MS, Self::relay_fallback_delay_ms(),
|r| r.3, |r| r.3,
) )
.await .await
@@ -2053,6 +2073,8 @@ pub struct AudioHandler {
simple: Option<psimple::Simple>, simple: Option<psimple::Simple>,
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
audio_buffer: AudioBuffer, audio_buffer: AudioBuffer,
#[cfg(not(target_os = "linux"))]
audio_resampler: Option<crate::audio_resampler::AudioResampler>,
sample_rate: (u32, u32), sample_rate: (u32, u32),
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
audio_stream: Option<Box<dyn StreamTrait>>, audio_stream: Option<Box<dyn StreamTrait>>,
@@ -2060,7 +2082,55 @@ pub struct AudioHandler {
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
device_channel: u16, device_channel: u16,
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
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"))] #[cfg(not(target_os = "linux"))]
@@ -2068,6 +2138,7 @@ struct AudioBuffer(
pub Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>, pub Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>,
usize, usize,
[usize; 30], [usize; 30],
Arc<std::sync::atomic::AtomicUsize>,
); );
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
@@ -2079,6 +2150,7 @@ impl Default for AudioBuffer {
)), )),
48000 * 2, 48000 * 2,
[0; 30], [0; 30],
Arc::new(std::sync::atomic::AtomicUsize::new(0)),
) )
} }
} }
@@ -2153,27 +2225,36 @@ impl AudioBuffer {
let skip = (cap * max / (30 * N) + 1) & (!1); let skip = (cap * max / (30 * N) + 1) & (!1);
if (having > skip * 3) && (skip > 0) { if (having > skip * 3) && (skip > 0) {
lock.skip(skip); lock.skip(skip);
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 /// append pcm to audio buffer, if buffered data
/// exceeds AUDIO_BUFFER_MS, only AUDIO_BUFFER_MS /// exceeds AUDIO_BUFFER_MS, only AUDIO_BUFFER_MS
/// will be kept. /// will be kept.
fn append_pcm2(&self, buffer: &[f32]) -> usize { fn append_pcm2(&self, buffer: &[f32]) -> usize {
let mut lock = self.0.lock().unwrap(); let mut lock = self.0.lock().unwrap();
let cap = lock.capacity(); let cap = lock.capacity();
if buffer.len() > cap {
lock.push_slice_overwrite(buffer);
return cap;
}
let having = lock.occupied_len() + buffer.len(); let having = lock.occupied_len() + buffer.len();
if having > cap {
lock.skip(having - cap);
}
lock.push_slice_overwrite(buffer); 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 /// 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 { impl AudioHandler {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> {
@@ -2238,6 +2354,9 @@ impl AudioHandler {
} }
self.sample_rate = (format0.sample_rate, config.sample_rate.0); self.sample_rate = (format0.sample_rate, config.sample_rate.0);
let audio_resampler = create_audio_resampler(
format0.sample_rate, config.sample_rate.0, format0.channels as _,
)?;
let mut build_output_stream = |config: StreamConfig| match sample_format { let mut build_output_stream = |config: StreamConfig| match sample_format {
cpal::SampleFormat::I8 => self.build_output_stream::<i8>(&config, &device), cpal::SampleFormat::I8 => self.build_output_stream::<i8>(&config, &device),
cpal::SampleFormat::I16 => self.build_output_stream::<i16>(&config, &device), cpal::SampleFormat::I16 => self.build_output_stream::<i16>(&config, &device),
@@ -2262,6 +2381,7 @@ impl AudioHandler {
} else { } else {
build_output_stream(config)?; build_output_stream(config)?;
} }
self.audio_resampler = audio_resampler;
Ok(()) Ok(())
} }
@@ -2274,10 +2394,17 @@ impl AudioHandler {
} }
match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) { match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) {
Ok(d) => { Ok(d) => {
#[cfg(target_os = "linux")]
let keep_existing_stream = self.simple.is_some()
&& self.sample_rate.0 == f.sample_rate
&& u32::from(self.channels) == f.channels;
#[cfg(not(target_os = "linux"))]
let keep_existing_stream = false;
let buffer = vec![0.; f.sample_rate as usize * f.channels as usize]; let buffer = vec![0.; f.sample_rate as usize * f.channels as usize];
self.audio_decoder = Some((d, buffer)); self.audio_decoder = Some((d, buffer));
self.channels = f.channels as _; self.channels = f.channels as _;
allow_err!(self.start_audio(f)); let result = self.start_audio(f);
self.handle_audio_start_result(result, keep_existing_stream);
} }
Err(err) => { Err(err) => {
log::error!("Failed to create audio decoder: {}", err); log::error!("Failed to create audio decoder: {}", err);
@@ -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. /// Handle audio frame and play it.
#[inline] #[inline]
pub fn handle_frame(&mut self, frame: AudioFrame) { pub fn handle_frame(&mut self, frame: AudioFrame) {
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
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; return;
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -2298,31 +2445,33 @@ impl AudioHandler {
return; return;
} }
self.audio_decoder.as_mut().map(|(d, buffer)| { self.audio_decoder.as_mut().map(|(d, buffer)| {
if let Ok(n) = d.decode_float(&frame.data, buffer, false) { 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 channels = self.channels;
let n = n * (channels as usize); let n = decoded_frames * channels as usize;
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
{ {
let sample_rate0 = self.sample_rate.0; let config = DecodedAudioConfig {
let sample_rate = self.sample_rate.1; sample_rate: self.sample_rate.1,
let mut buffer = buffer[0..n].to_owned(); input_channels: self.channels,
if sample_rate != sample_rate0 { output_channels: self.device_channel,
buffer = crate::audio_resample( };
let buffer = match prepare_decoded_audio(
&buffer[0..n], &buffer[0..n],
sample_rate0, self.audio_resampler.as_mut(),
sample_rate, config,
channels, ) {
); Ok(output) => output,
} Err(error) => {
if self.channels != self.device_channel { log::error!("Failed to resample decoded audio: {error:#}");
buffer = crate::audio_rechannel( return;
buffer,
sample_rate,
sample_rate,
self.channels,
self.device_channel,
);
} }
};
self.audio_buffer.append_pcm(&buffer); self.audio_buffer.append_pcm(&buffer);
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -2331,7 +2480,6 @@ impl AudioHandler {
unsafe { std::slice::from_raw_parts::<u8>(buffer.as_ptr() as _, n * 4) }; unsafe { std::slice::from_raw_parts::<u8>(buffer.as_ptr() as _, n * 4) };
self.simple.as_mut().map(|x| x.write(data_u8)); self.simple.as_mut().map(|x| x.write(data_u8));
} }
}
}); });
} }
@@ -2350,63 +2498,28 @@ impl AudioHandler {
self.audio_buffer self.audio_buffer
.resize(config.sample_rate.0 as _, config.channels as _); .resize(config.sample_rate.0 as _, config.channels as _);
let audio_buffer = self.audio_buffer.0.clone(); let audio_buffer = self.audio_buffer.0.clone();
let 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 timeout = None;
let stream = device.build_output_stream( let stream = device.build_output_stream(
config, config,
move |data: &mut [T], info: &cpal::OutputCallbackInfo| { move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
if !*ready.lock().unwrap() { playback_writer.write_output(data);
*ready.lock().unwrap() = true;
}
let mut n = data.len();
let mut lock = audio_buffer.lock().unwrap();
let mut having = lock.occupied_len();
// android two timestamps, one from zero, another not
#[cfg(not(target_os = "android"))]
if having < n {
let tms = info.timestamp();
let how_long = tms
.playback
.duration_since(&tms.callback)
.unwrap_or(Duration::from_millis(0));
// must long enough to fight back scheuler delay
if how_long > Duration::from_millis(6) && how_long < Duration::from_millis(3000)
{
drop(lock);
std::thread::sleep(how_long.div_f32(1.2));
lock = audio_buffer.lock().unwrap();
having = lock.occupied_len();
}
if having < n {
n = having;
}
}
#[cfg(target_os = "android")]
if having < n {
n = having;
}
let mut elems = vec![0.0f32; n];
if n > 0 {
lock.pop_slice(&mut elems);
}
drop(lock);
let mut input = elems.into_iter();
for sample in data.iter_mut() {
*sample = match input.next() {
Some(x) => T::from_sample(x),
_ => T::from_sample(0.),
};
}
}, },
err_fn, err_fn,
timeout, timeout,
)?; )?;
stream.play()?; stream.play()?;
self.audio_stream = Some(Box::new(stream)); self.audio_stream = Some(Box::new(stream));
self.playback_status = playback_status;
Ok(()) Ok(())
} }
} }
@@ -2426,6 +2539,27 @@ mod audio_format_tests {
assert!(!is_supported_audio_channel_count(0)); assert!(!is_supported_audio_channel_count(0));
assert!(!is_supported_audio_channel_count(u32::MAX)); assert!(!is_supported_audio_channel_count(u32::MAX));
} }
#[test]
fn failed_audio_start_discards_format_state() {
use super::{anyhow, AudioDecoder, AudioHandler, Stereo};
const SAMPLE_RATE: u32 = 48_000;
const CHANNELS: u16 = 2;
let decoder = AudioDecoder::new(SAMPLE_RATE, Stereo).unwrap();
let mut handler = AudioHandler {
audio_decoder: Some((decoder, Vec::new())),
sample_rate: (SAMPLE_RATE, SAMPLE_RATE),
channels: CHANNELS,
..Default::default()
};
handler.handle_audio_start_result(Err(anyhow!("Injected playback startup failure")), false);
assert!(handler.audio_decoder.is_none());
assert_eq!(handler.channels, 0);
assert_eq!(handler.sample_rate, (0, 0));
}
} }
/// Video handler for the [`Client`]. /// Video handler for the [`Client`].

View File

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

@@ -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")] #[cfg(feature = "use_dasp")]
pub fn audio_resample( pub fn audio_resample(
data: &[f32], 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( pub fn audio_resample(
data: &[f32], data: &[f32],
sample_rate0: u32, sample_rate0: u32,

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "انتهى طلب مشاركة الشاشة على الجهاز البعيد دون أن يكتمل"), ("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 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 اللازم لالتقاط الشاشة ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "تعذّر على RustDesk تحميل مكوّن GStreamer اللازم لالتقاط الشاشة ({})"),
("Relay fallback delay in seconds", "مهلة التراجع إلى الترحيل بالثواني"),
("relay-fallback-delay-tip", "المدة التي ينتظرها اتصال الترحيل القائم بالفعل الاتصالَ المباشر عبر WebRTC قبل أن يُستخدم بدلًا منه. زِدها لمنح الاتصال المباشر البطيء فرصة أكبر للفوز؛ وقلّلها للاستقرار على الترحيل أسرع في الشبكات التي يتعذر فيها الاتصال المباشر. اتركها فارغة للقيمة الافتراضية 2.5 ثانية."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -770,5 +770,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("port-forward-mux-tip", "Port yönləndirmə xəritələnməsinin hər əlaqəsini qarşı tərəfə açılan tək əlaqə üzərindən daşıyır, hər biri üçün yenidən qoşulub giriş etmək əvəzinə."), ("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 WebRTC P2P connection", "WebRTC P2P əlaqəsini aktivləşdir"),
("Enable TCP hole punching", "TCP deşik açmanı 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(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Запыт на абагульванне экрана на аддаленай прыладзе завяршыўся, не будучы выкананым"), ("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 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, патрэбны для захопу экрана ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не змог загрузіць кампанент GStreamer, патрэбны для захопу экрана ({})"),
("Relay fallback delay in seconds", "Затрымка пераходу на рэтранслятар у секундах"),
("relay-fallback-delay-tip", "Колькі часу ўжо ўсталяванае злучэнне праз рэтранслятар чакае прамога злучэння WebRTC, перш чым будзе выкарыстана замест яго. Павялічце, каб даць павольнаму прамому злучэнню больш часу; паменшыце, каб хутчэй пераходзіць на рэтранслятар у сетках, дзе прамое злучэнне немагчымае. Пакіньце пустым для значэння па змаўчанні 2.5 секунды."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Заявката за споделяне на екрана на отдалеченото устройство приключи, без да бъде изпълнена"), ("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 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, необходим за заснемане на екрана ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не можа да зареди компонент на GStreamer, необходим за заснемане на екрана ({})"),
("Relay fallback delay in seconds", "Забавяне преди преминаване към препредаване в секунди"),
("relay-fallback-delay-tip", "Колко време вече установената връзка чрез препредаване изчаква директната WebRTC връзка, преди да бъде използвана вместо нея. Увеличете, за да дадете повече време на бавна директна връзка; намалете, за да се премине по-бързо към препредаване в мрежи, където директна връзка е невъзможна. Оставете празно за стойността по подразбиране 2.5 секунди."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "La sol·licitud de compartició de pantalla al dispositiu remot ha acabat sense completar-se"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "El RustDesk no ha pogut carregar un component del GStreamer necessari per capturar la pantalla ({})"),
("Relay fallback delay in seconds", "Retard abans de recórrer al relé en segons"),
("relay-fallback-delay-tip", "Quant de temps espera una connexió de relé ja establerta la connexió directa WebRTC abans d'utilitzar-se en lloc seu. Augmenteu-lo per donar més temps a una connexió directa lenta; reduïu-lo per passar abans al relé en xarxes on no es pot fer una connexió directa. Deixeu-lo buit per al valor predeterminat de 2.5 segons."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "远程设备上的屏幕共享请求已结束,但未完成"), ("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 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 组件 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk 无法加载屏幕捕获所需的 GStreamer 组件 ({})"),
("Relay fallback delay in seconds", "回落到中继前的等待时间(秒)"),
("relay-fallback-delay-tip", "已经建立的中继连接会等待直连的 WebRTC 多久,超过这个时间就改用中继。调大可以让较慢的直连有更多机会胜出;调小则在无法直连的网络上更快回落到中继。留空表示使用默认值 2.5 秒。"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Žádost o sdílení obrazovky na vzdáleném zařízení skončila, aniž by byla dokončena"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nemohl načíst komponentu GStreameru potřebnou k zachycení obrazovky ({})"),
("Relay fallback delay in seconds", "Prodleva před přepnutím na přenos v sekundách"),
("relay-fallback-delay-tip", "Jak dlouho již navázané spojení přes přenos čeká na přímé spojení WebRTC, než bude použito místo něj. Zvyšte, aby pomalé přímé spojení mělo více času uspět; snižte, aby se v sítích, kde přímé spojení není možné, dříve přešlo na přenos. Ponechte prázdné pro výchozí hodnotu 2.5 sekundy."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Anmodningen om skærmdeling på fjernenheden sluttede uden at blive gennemført"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kunne ikke indlæse en GStreamer-komponent, der kræves til skærmoptagelse ({})"),
("Relay fallback delay in seconds", "Forsinkelse før brug af relæ i sekunder"),
("relay-fallback-delay-tip", "Hvor længe en allerede oprettet relæforbindelse venter på den direkte WebRTC-forbindelse, før den bruges i stedet. Forøg for at give en langsom direkte forbindelse mere tid; sænk for hurtigere at falde tilbage til relæet på netværk, hvor en direkte forbindelse ikke kan oprettes. Lad feltet stå tomt for standardværdien 2.5 sekunder."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Die Anfrage zur Bildschirmfreigabe auf dem entfernten Gerät endete, ohne abgeschlossen zu werden"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk konnte eine für die Bildschirmaufnahme benötigte GStreamer-Komponente nicht laden ({})"),
("Relay fallback delay in seconds", "Verzögerung bis zum Relais in Sekunden"),
("relay-fallback-delay-tip", "Wie lange eine bereits aufgebaute Relaisverbindung auf die direkte WebRTC-Verbindung wartet, bevor sie stattdessen verwendet wird. Erhöhen Sie den Wert, um einer langsamen direkten Verbindung mehr Zeit zu geben; verringern Sie ihn, um in Netzwerken ohne mögliche Direktverbindung schneller auf das Relais zurückzufallen. Leer lassen für den Standardwert von 2.5 Sekunden."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Το αίτημα κοινής χρήσης οθόνης στην απομακρυσμένη συσκευή έληξε χωρίς να ολοκληρωθεί"), ("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 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 που απαιτείται για την καταγραφή οθόνης ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "Το RustDesk δεν μπόρεσε να φορτώσει ένα στοιχείο του GStreamer που απαιτείται για την καταγραφή οθόνης ({})"),
("Relay fallback delay in seconds", "Καθυστέρηση πριν από τη χρήση αναμεταδότη σε δευτερόλεπτα"),
("relay-fallback-delay-tip", "Πόσο χρόνο περιμένει μια ήδη ενεργή σύνδεση αναμεταδότη την απευθείας σύνδεση WebRTC πριν χρησιμοποιηθεί στη θέση της. Αυξήστε το για να δώσετε σε μια αργή απευθείας σύνδεση περισσότερο χρόνο. Μειώστε το για ταχύτερη επιστροφή στον αναμεταδότη σε δίκτυα όπου δεν είναι δυνατή η απευθείας σύνδεση. Αφήστε το κενό για την προεπιλογή των 2.5 δευτερολέπτων."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -278,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."), ("sync-clipboard-between-sessions-tip", "Text or images copied in one remote session are also sent to the clipboard of your other connected sessions."),
("terminal-clipboard-write-tip", "An app in the terminal wants to copy text to this device's clipboard. If granted, this permission applies to terminal apps in all connections until you turn it off in Settings. Manual copy and paste are unaffected."), ("terminal-clipboard-write-tip", "An app in the terminal wants to copy text to this device's clipboard. If granted, this permission applies to terminal apps in all connections until you turn it off in Settings. Manual copy and paste are unaffected."),
("port-forward-mux-tip", "Carry every connection of a port-forward mapping over a single connection to the peer, instead of connecting and logging in again for each one."), ("port-forward-mux-tip", "Carry every connection of a port-forward mapping over a single connection to the peer, instead of connecting and logging in again for each one."),
("relay-fallback-delay-tip", "How long a relay connection that is already up waits for the direct WebRTC connection before it is used instead. Raise it to give a slow direct connection more time to win; lower it to settle on the relay sooner on networks where a direct connection cannot be made. Leave empty for the default of 2.5 seconds."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "La peto pri ekrandividado sur la fora aparato finiĝis sen kompletiĝi"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ne povis ŝargi komponanton de GStreamer necesan por ekrankapto ({})"),
("Relay fallback delay in seconds", "Prokrasto antaŭ retransmisio en sekundoj"),
("relay-fallback-delay-tip", "Kiom longe jam establita retransmisia konekto atendas la rektan WebRTC-konekton antaŭ ol esti uzata anstataŭe. Pligrandigu ĝin por doni al malrapida rekta konekto pli da tempo; malpligrandigu ĝin por pli frue uzi la retransmision en retoj kie rekta konekto ne eblas. Lasu malplena por la defaŭlta valoro de 2.5 sekundoj."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "La solicitud de compartir pantalla en el dispositivo remoto terminó sin completarse"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk no ha podido cargar un componente de GStreamer necesario para capturar la pantalla ({})"),
("Relay fallback delay in seconds", "Retardo antes de usar el relé en segundos"),
("relay-fallback-delay-tip", "Cuánto tiempo espera una conexión de relé ya establecida a la conexión directa WebRTC antes de usarse en su lugar. Auméntelo para dar más tiempo a una conexión directa lenta; redúzcalo para recurrir antes al relé en redes donde no es posible una conexión directa. Déjelo vacío para el valor predeterminado de 2.5 segundos."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Ekraani jagamise taotlus kaugseadmes lõppes ilma lõpule jõudmata"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ei suutnud laadida ekraani jäädvustamiseks vajalikku GStreameri komponenti ({})"),
("Relay fallback delay in seconds", "Viivitus enne relee kasutamist sekundites"),
("relay-fallback-delay-tip", "Kui kaua juba loodud releeühendus ootab otsest WebRTC-ühendust, enne kui seda selle asemel kasutatakse. Suurendage, et anda aeglasele otseühendusele rohkem aega; vähendage, et võrkudes, kus otseühendust luua ei saa, releele kiiremini üle minna. Jätke tühjaks vaikeväärtuse 2.5 sekundit kasutamiseks."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Urruneko gailuko pantaila partekatzeko eskaera osatu gabe amaitu da"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk-ek ezin izan du pantaila kapturatzeko beharrezkoa den GStreamer osagai bat kargatu ({})"),
("Relay fallback delay in seconds", "Errelera itzultzeko atzerapena segundotan"),
("relay-fallback-delay-tip", "Dagoeneko ezarritako errele-konexio batek WebRTC konexio zuzenari zenbat denbora itxaroten dion, haren ordez erabili aurretik. Handitu konexio zuzen motel bati denbora gehiago emateko; txikitu konexio zuzena egin ezin den sareetan lehenago errelera itzultzeko. Utzi hutsik 2.5 segundoko balio lehenetsirako."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "درخواست اشتراک‌گذاری صفحه در دستگاه راه دور بدون تکمیل شدن پایان یافت"), ("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 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 موردنیاز برای ضبط صفحه را بارگذاری کند ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk نتوانست مؤلفه GStreamer موردنیاز برای ضبط صفحه را بارگذاری کند ({})"),
("Relay fallback delay in seconds", "تأخیر بازگشت به رله بر حسب ثانیه"),
("relay-fallback-delay-tip", "یک اتصال رله که از قبل برقرار شده چقدر منتظر اتصال مستقیم WebRTC می ماند پیش از آنکه به جای آن استفاده شود. آن را افزایش دهید تا به اتصال مستقیم کند فرصت بیشتری داده شود؛ کاهش دهید تا در شبکه هایی که اتصال مستقیم ممکن نیست، زودتر به رله بازگردد. برای مقدار پیش فرض 2.5 ثانیه خالی بگذارید."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Näytön jakamispyyntö etälaitteessa päättyi ilman että se saatiin valmiiksi"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ei voinut ladata näytön kaappaukseen tarvittavaa GStreamer-osaa ({})"),
("Relay fallback delay in seconds", "Viive ennen välitykseen siirtymistä sekunteina"),
("relay-fallback-delay-tip", "Kuinka kauan jo muodostettu välitysyhteys odottaa suoraa WebRTC-yhteyttä ennen kuin sitä käytetään sen sijaan. Kasvata arvoa antaaksesi hitaalle suoralle yhteydelle enemmän aikaa; pienennä sitä siirtyäksesi nopeammin välitykseen verkoissa, joissa suoraa yhteyttä ei voi muodostaa. Jätä tyhjäksi käyttääksesi oletusarvoa 2.5 sekuntia."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "La demande de partage d'écran sur l'appareil distant s'est terminée sans aboutir"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk n'a pas pu charger un composant GStreamer nécessaire à la capture d'écran ({})"),
("Relay fallback delay in seconds", "Délai avant bascule vers le relais en secondes"),
("relay-fallback-delay-tip", "Durée pendant laquelle une connexion relais déjà établie attend la connexion directe WebRTC avant d'être utilisée à sa place. Augmentez-la pour laisser plus de temps à une connexion directe lente ; diminuez-la pour basculer plus tôt vers le relais sur les réseaux où une connexion directe est impossible. Laissez vide pour la valeur par défaut de 2.5 secondes."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "ეკრანის გაზიარების მოთხოვნა დისტანციურ მოწყობილობაზე დასრულდა შეუსრულებლად"), ("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 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-ის კომპონენტი ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk-მა ვერ ჩატვირთა ეკრანის ჩაწერისთვის საჭირო GStreamer-ის კომპონენტი ({})"),
("Relay fallback delay in seconds", "რელეზე გადასვლის დაყოვნება წამებში"),
("relay-fallback-delay-tip", "რამდენ ხანს ელოდება უკვე დამყარებული რელე-კავშირი პირდაპირ WebRTC კავშირს, სანამ მის ნაცვლად გამოიყენება. გაზარდეთ, რომ ნელ პირდაპირ კავშირს მეტი დრო მისცეთ; შეამცირეთ, რომ ქსელებში, სადაც პირდაპირი კავშირი შეუძლებელია, უფრო სწრაფად გადავიდეს რელეზე. დატოვეთ ცარიელი ნაგულისხმევი 2.5 წამისთვის."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "A solicitude de compartir pantalla no dispositivo remoto rematou sen completarse"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk non puido cargar un compoñente de GStreamer necesario para capturar a pantalla ({})"),
("Relay fallback delay in seconds", "Atraso antes de usar o relé en segundos"),
("relay-fallback-delay-tip", "Canto tempo agarda unha conexión de relé xa establecida pola conexión directa WebRTC antes de usarse no seu lugar. Auménteo para darlle máis tempo a unha conexión directa lenta; redúzao para recorrer antes ao relé en redes onde non é posible unha conexión directa. Déixeo baleiro para o valor predeterminado de 2.5 segundos."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "રિમોટ ઉપકરણ પર સ્ક્રીન શેરિંગ વિનંતી પૂર્ણ થયા વિના સમાપ્ત થઈ"), ("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 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 ઘટક લોડ કરી શક્યું નથી ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk સ્ક્રીન કૅપ્ચર માટે જરૂરી GStreamer ઘટક લોડ કરી શક્યું નથી ({})"),
("Relay fallback delay in seconds", "રિલે પર પાછા ફરવામાં વિલંબ સેકન્ડમાં"),
("relay-fallback-delay-tip", "પહેલેથી સ્થાપિત રિલે કનેક્શન સીધા WebRTC કનેક્શનની કેટલો સમય રાહ જુએ છે, ત્યાર બાદ તેના બદલે વપરાય છે. ધીમા સીધા કનેક્શનને વધુ સમય આપવા માટે વધારો; જ્યાં સીધું કનેક્શન શક્ય નથી તેવા નેટવર્ક પર વહેલા રિલે પર જવા માટે ઘટાડો. મૂળભૂત 2.5 સેકન્ડ માટે ખાલી રાખો."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "בקשת שיתוף המסך במכשיר המרוחק הסתיימה מבלי להתבצע"), ("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 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 הדרוש ללכידת מסך ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk לא הצליח לטעון רכיב GStreamer הדרוש ללכידת מסך ({})"),
("Relay fallback delay in seconds", "השהיה לפני מעבר לממסר בשניות"),
("relay-fallback-delay-tip", "כמה זמן חיבור ממסר שכבר נוצר ממתין לחיבור WebRTC הישיר לפני שישמש במקומו. הגדל כדי לתת לחיבור ישיר איטי יותר זמן; הקטן כדי לעבור מהר יותר לממסר ברשתות שבהן לא ניתן ליצור חיבור ישיר. השאר ריק לערך ברירת המחדל של 2.5 שניות."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "रिमोट डिवाइस पर स्क्रीन शेयरिंग अनुरोध पूरा हुए बिना समाप्त हो गया"), ("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 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 घटक लोड नहीं कर सका ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk स्क्रीन कैप्चर के लिए आवश्यक GStreamer घटक लोड नहीं कर सका ({})"),
("Relay fallback delay in seconds", "रिले पर लौटने में विलंब सेकंड में"),
("relay-fallback-delay-tip", "पहले से स्थापित रिले कनेक्शन सीधे WebRTC कनेक्शन की कितनी देर प्रतीक्षा करता है, उसके बाद उसके स्थान पर उपयोग किया जाता है। धीमे सीधे कनेक्शन को अधिक समय देने के लिए बढ़ाएँ; जिन नेटवर्क पर सीधा कनेक्शन संभव नहीं है वहाँ जल्दी रिले पर जाने के लिए घटाएँ। डिफ़ॉल्ट 2.5 सेकंड के लिए खाली छोड़ें।"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Zahtjev za dijeljenje zaslona na udaljenom uređaju završio je bez dovršetka"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nije mogao učitati GStreamer komponentu potrebnu za snimanje zaslona ({})"),
("Relay fallback delay in seconds", "Odgoda prije prelaska na relej u sekundama"),
("relay-fallback-delay-tip", "Koliko dugo već uspostavljena relejna veza čeka izravnu WebRTC vezu prije nego što se upotrijebi umjesto nje. Povećajte da sporoj izravnoj vezi date više vremena; smanjite da se na mrežama gdje izravna veza nije moguća brže prijeđe na relej. Ostavite prazno za zadanu vrijednost od 2.5 sekunde."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "A képernyőmegosztási kérés a távoli eszközön befejeződött anélkül, hogy teljesült volna"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "A RustDesk nem tudta betölteni a képernyőrögzítéshez szükséges GStreamer összetevőt ({})"),
("Relay fallback delay in seconds", "Késleltetés a továbbítóra váltás előtt másodpercben"),
("relay-fallback-delay-tip", "Mennyi ideig vár a már létrejött továbbító kapcsolat a közvetlen WebRTC kapcsolatra, mielőtt helyette használnák. Növelje, hogy a lassú közvetlen kapcsolatnak több ideje legyen; csökkentse, hogy olyan hálózatokon, ahol közvetlen kapcsolat nem hozható létre, hamarabb váltson továbbítóra. Hagyja üresen az alapértelmezett 2.5 másodperchez."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Permintaan berbagi layar di perangkat jarak jauh berakhir tanpa diselesaikan"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk tidak dapat memuat komponen GStreamer yang diperlukan untuk merekam layar ({})"),
("Relay fallback delay in seconds", "Jeda sebelum beralih ke relai dalam detik"),
("relay-fallback-delay-tip", "Berapa lama koneksi relai yang sudah terbentuk menunggu koneksi langsung WebRTC sebelum digunakan sebagai gantinya. Perbesar untuk memberi koneksi langsung yang lambat lebih banyak waktu; perkecil agar lebih cepat beralih ke relai pada jaringan yang tidak memungkinkan koneksi langsung. Biarkan kosong untuk nilai bawaan 2.5 detik."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -541,7 +541,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Plug out all", "Scollega tutto"), ("Plug out all", "Scollega tutto"),
("True color (4:4:4)", "Colore reale (4:4:4)"), ("True color (4:4:4)", "Colore reale (4:4:4)"),
("Enable blocking user input", "Abilita blocco input utente"), ("Enable blocking user input", "Abilita blocco input utente"),
("id_input_tip", "Puoi inserire un ID, un IP diretto o un dominio con una porta (<dominio>:<porta>).\nSe vuoi accedere 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_mag_tip", "Modo 1"),
("privacy_mode_impl_virtual_display_tip", "Modo 2"), ("privacy_mode_impl_virtual_display_tip", "Modo 2"),
("Enter privacy mode", "Entra in modalità privacy"), ("Enter privacy mode", "Entra in modalità privacy"),
@@ -770,13 +770,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("port-forward-mux-tip", "Fa passare tutte le connessioni di un inoltro porte in un'unica connessione verso il dispositivo remoto, invece di connettersi e autenticarsi di nuovo per ognuna."), ("port-forward-mux-tip", "Fa passare tutte le connessioni di un inoltro porte in un'unica connessione verso il dispositivo remoto, invece di connettersi e autenticarsi di nuovo per ognuna."),
("Enable WebRTC P2P connection", "Abilita connessione P2P WebRTC"), ("Enable WebRTC P2P connection", "Abilita connessione P2P WebRTC"),
("Enable TCP hole punching", "Abilita hole punching TCP"), ("Enable TCP hole punching", "Abilita hole punching TCP"),
("The screen sharing request was declined on the remote device", ""), ("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", ""), ("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 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", ""), ("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", ""), ("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", ""), ("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 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 could not load a GStreamer component needed for screen capture ({})", "RustDesk non è riuscito a caricare un componente GStreamer necessario per l'acquisizione dello schermo ({})"),
("Relay fallback delay in seconds", ""),
("relay-fallback-delay-tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "リモート端末での画面共有の要求は完了しないまま終了しました"), ("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 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 コンポーネントを読み込めませんでした ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk は画面キャプチャに必要な GStreamer コンポーネントを読み込めませんでした ({})"),
("Relay fallback delay in seconds", "中継に切り替えるまでの待ち時間 (秒)"),
("relay-fallback-delay-tip", "すでに確立された中継接続が、直接の WebRTC 接続をどれだけ待ってから代わりに使用されるかを指定します。値を大きくすると遅い直接接続に時間を与えられ、小さくすると直接接続できないネットワークで早く中継に切り替わります。空欄にすると既定値の 2.5 秒になります。"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "원격 장치의 화면 공유 요청이 완료되지 않은 채 종료되었습니다"), ("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 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 구성 요소를 불러오지 못했습니다 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk가 화면 캡처에 필요한 GStreamer 구성 요소를 불러오지 못했습니다 ({})"),
("Relay fallback delay in seconds", "중계로 전환하기까지의 대기 시간(초)"),
("relay-fallback-delay-tip", "이미 연결된 중계 연결이 직접 WebRTC 연결을 얼마나 기다린 후 대신 사용되는지입니다. 값을 늘리면 느린 직접 연결에 더 많은 시간을 주고, 줄이면 직접 연결이 불가능한 네트워크에서 더 빨리 중계로 전환합니다. 비워 두면 기본값 2.5초가 사용됩니다."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Қашықтағы құрылғыдағы экранды бөлісу сұрауы аяқталмай тоқтады"), ("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 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 компонентін жүктей алмады ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk экранды түсіру үшін қажет GStreamer компонентін жүктей алмады ({})"),
("Relay fallback delay in seconds", "Релеге ауысу кідірісі, секундпен"),
("relay-fallback-delay-tip", "Бұрыннан орнатылған реле байланысы тікелей WebRTC байланысын қанша уақыт күтеді, содан кейін оның орнына қолданылады. Баяу тікелей байланысқа көбірек уақыт беру үшін үлкейтіңіз; тікелей байланыс мүмкін емес желілерде релеге тезірек ауысу үшін кішірейтіңіз. Әдепкі 2.5 секунд үшін бос қалдырыңыз."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Ekrano bendrinimo užklausa nuotoliniame įrenginyje baigėsi jos neužbaigus"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nepavyko įkelti ekrano įrašymui reikalingo GStreamer komponento ({})"),
("Relay fallback delay in seconds", "Delsa prieš pereinant prie perdavimo sekundėmis"),
("relay-fallback-delay-tip", "Kiek laiko jau užmegztas perdavimo ryšys laukia tiesioginio WebRTC ryšio, kol bus panaudotas vietoj jo. Padidinkite, kad lėtam tiesioginiam ryšiui būtų skirta daugiau laiko; sumažinkite, kad tinkluose, kuriuose tiesioginis ryšys neįmanomas, greičiau būtų pereinama prie perdavimo. Palikite tuščią numatytajai 2.5 sekundės reikšmei."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Ekrāna koplietošanas pieprasījums attālinātajā ierīcē beidzās, netiekot pabeigts"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nevarēja ielādēt ekrāna tveršanai nepieciešamo GStreamer komponentu ({})"),
("Relay fallback delay in seconds", "Aizkave pirms pārslēgšanās uz retranslatoru sekundēs"),
("relay-fallback-delay-tip", "Cik ilgi jau izveidots retranslatora savienojums gaida tiešo WebRTC savienojumu, pirms tiek izmantots tā vietā. Palieliniet, lai lēnam tiešajam savienojumam dotu vairāk laika; samaziniet, lai tīklos, kur tiešais savienojums nav iespējams, ātrāk pārslēgtos uz retranslatoru. Atstājiet tukšu noklusējuma 2.5 sekunžu vērtībai."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "വിദൂര ഉപകരണത്തിലെ സ്ക്രീൻ പങ്കിടൽ അഭ്യർത്ഥന പൂർത്തിയാകാതെ അവസാനിച്ചു"), ("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 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-ന് ലോഡ് ചെയ്യാനായില്ല ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "സ്ക്രീൻ പകർത്താൻ ആവശ്യമായ GStreamer ഘടകം RustDesk-ന് ലോഡ് ചെയ്യാനായില്ല ({})"),
("Relay fallback delay in seconds", "റിലേയിലേക്ക് മാറുന്നതിനുള്ള കാലതാമസം സെക്കൻഡിൽ"),
("relay-fallback-delay-tip", "ഇതിനകം സ്ഥാപിതമായ റിലേ കണക്ഷൻ നേരിട്ടുള്ള WebRTC കണക്ഷനായി എത്ര നേരം കാത്തിരിക്കുന്നു, അതിനുശേഷം അതിനുപകരം ഉപയോഗിക്കുന്നു. മന്ദഗതിയിലുള്ള നേരിട്ടുള്ള കണക്ഷന് കൂടുതൽ സമയം നൽകാൻ വർദ്ധിപ്പിക്കുക; നേരിട്ടുള്ള കണക്ഷൻ സാധ്യമല്ലാത്ത നെറ്റ്‌വർക്കുകളിൽ വേഗത്തിൽ റിലേയിലേക്ക് മാറാൻ കുറയ്ക്കുക. സ്ഥിരസ്ഥിതിയായ 2.5 സെക്കൻഡിനായി ശൂന്യമാക്കിയിടുക."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Forespørselen om skjermdeling på den eksterne enheten ble avsluttet uten å bli fullført"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kunne ikke laste en GStreamer-komponent som kreves for skjermopptak ({})"),
("Relay fallback delay in seconds", "Forsinkelse før bruk av relé i sekunder"),
("relay-fallback-delay-tip", "Hvor lenge en allerede opprettet reléforbindelse venter på den direkte WebRTC-forbindelsen før den brukes i stedet. Øk verdien for å gi en treg direkteforbindelse mer tid; senk den for å gå raskere over til reléet på nettverk der direkte forbindelse ikke er mulig. La stå tom for standardverdien på 2.5 sekunder."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Het verzoek om schermdeling op het externe apparaat is geëindigd zonder te zijn voltooid"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kon een GStreamer-component die nodig is voor schermopname niet laden ({})"),
("Relay fallback delay in seconds", "Vertraging voordat relay wordt gebruikt in seconden"),
("relay-fallback-delay-tip", "Hoe lang een al tot stand gekomen relayverbinding wacht op de directe WebRTC-verbinding voordat deze in plaats daarvan wordt gebruikt. Verhoog de waarde om een trage directe verbinding meer tijd te geven; verlaag deze om op netwerken waar een directe verbinding niet mogelijk is sneller op de relay terug te vallen. Laat leeg voor de standaardwaarde van 2.5 seconden."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Żądanie udostępnienia ekranu na urządzeniu zdalnym zakończyło się bez ukończenia"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nie mógł załadować składnika GStreamer wymaganego do przechwytywania ekranu ({})"),
("Relay fallback delay in seconds", "Opóźnienie przed przejściem na przekaźnik w sekundach"),
("relay-fallback-delay-tip", "Jak długo nawiązane już połączenie przez przekaźnik czeka na bezpośrednie połączenie WebRTC, zanim zostanie użyte zamiast niego. Zwiększ, aby dać wolnemu połączeniu bezpośredniemu więcej czasu; zmniejsz, aby w sieciach, w których połączenie bezpośrednie jest niemożliwe, szybciej przechodzić na przekaźnik. Pozostaw puste, aby użyć wartości domyślnej 2.5 sekundy."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "O pedido de partilha de ecrã no dispositivo remoto terminou sem ser concluído"), ("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 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ã ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "O RustDesk não conseguiu carregar um componente do GStreamer necessário para capturar o ecrã ({})"),
("Relay fallback delay in seconds", "Atraso antes de recorrer ao retransmissor em segundos"),
("relay-fallback-delay-tip", "Quanto tempo uma ligação de retransmissão já estabelecida aguarda pela ligação direta WebRTC antes de ser usada em vez dela. Aumente para dar mais tempo a uma ligação direta lenta; diminua para recorrer mais cedo ao retransmissor em redes onde não é possível uma ligação direta. Deixe vazio para o valor predefinido de 2.5 segundos."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -691,7 +691,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Incorrect username or password.", "Usuário ou senha incorretos"), ("Incorrect username or password.", "Usuário ou senha incorretos"),
("The user is not an administrator.", "O usuário não é administrador"), ("The user is not an administrator.", "O usuário não é administrador"),
("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"), ("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"),
("Supported only in the installed version.", "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"), ("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"),
("Preparing for installation ...", "Preparando para instalação ..."), ("Preparing for installation ...", "Preparando para instalação ..."),
("Show my cursor", "Mostrar meu cursor"), ("Show my cursor", "Mostrar meu cursor"),
@@ -767,16 +767,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Allow terminal apps to copy to clipboard", ""), ("Allow terminal apps to copy to clipboard", ""),
("Enable", "Habilitar"), ("Enable", "Habilitar"),
("Reuse one connection for port forwarding", "Reutilizar uma conexão para encaminhamento de portas"), ("Reuse one connection for port forwarding", "Reutilizar uma conexão para encaminhamento de portas"),
("port-forward-mux-tip", "Levar todas as conexões de um encaminhamento de portas por uma única conexão com o outro computador, em vez de 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 WebRTC P2P connection", "Habilitar conexão WebRTC P2P"),
("Enable TCP hole punching", "Habilitar TCP hole punching"), ("Enable TCP hole punching", "Habilitar TCP hole punching"),
("The screen sharing request was declined on the remote device", "A solicitação de compartilhamento de tela foi recusada no dispositivo remoto"), ("The screen sharing request was declined on the remote device", "A solicitação de compartilhamento de tela foi recusada no dispositivo remoto"),
("The screen sharing request timed out on the remote device", "A solicitação de compartilhamento de tela expirou no dispositivo remoto"), ("The screen sharing request timed out on the remote device", "A solicitação de compartilhamento de tela expirou no dispositivo remoto"),
("RustDesk cannot reach the desktop session on the remote device, check that a desktop session is running and that RustDesk can use it", "O RustDesk não consegue acessar a sessão de área de trabalho no dispositivo remoto, verifique se há uma sessão em execução e se o RustDesk pode usá-la"), ("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", "Falta ao portal de área de trabalho do dispositivo remoto um recurso necessário para compartilhamento de tela ou controle remoto, seu backend pode não estar instalado"), ("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 aprovado no dispositivo remoto, mas não foi possível abrir a conexão PipeWire"), ("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 terminou sem ser concluída"), ("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 PipeWire pode ser muito antiga"), ("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 capturar a tela ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "O RustDesk não conseguiu carregar um componente do GStreamer necessário para a captura de tela ({})."),
("Relay fallback delay in seconds", "Atraso antes de recorrer ao retransmissor em segundos"),
("relay-fallback-delay-tip", "Quanto tempo uma conexão de retransmissão já estabelecida espera pela conexão direta WebRTC antes de ser usada no lugar dela. Aumente para dar mais tempo a uma conexão direta lenta; diminua para recorrer mais cedo ao retransmissor em redes onde não é possível uma conexão direta. Deixe vazio para o valor padrão de 2.5 segundos."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Cererea de partajare a ecranului pe dispozitivul de la distanță s-a încheiat fără a fi finalizată"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nu a putut încărca o componentă GStreamer necesară pentru capturarea ecranului ({})"),
("Relay fallback delay in seconds", "Întârziere înainte de trecerea la releu în secunde"),
("relay-fallback-delay-tip", "Cât timp așteaptă o conexiune prin releu deja stabilită conexiunea directă WebRTC înainte de a fi folosită în locul ei. Măriți valoarea pentru a acorda mai mult timp unei conexiuni directe lente; micșorați-o pentru a trece mai repede la releu în rețelele în care o conexiune directă nu este posibilă. Lăsați gol pentru valoarea implicită de 2.5 secunde."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Запрос на демонстрацию экрана на удалённом устройстве завершился, не будучи выполненным"), ("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 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, необходимый для захвата экрана ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не удалось загрузить компонент GStreamer, необходимый для захвата экрана ({})"),
("Relay fallback delay in seconds", "Задержка перед переходом на ретранслятор в секундах"),
("relay-fallback-delay-tip", "Сколько времени уже установленное соединение через ретранслятор ждёт прямое соединение WebRTC, прежде чем будет использовано вместо него. Увеличьте, чтобы дать медленному прямому соединению больше времени; уменьшите, чтобы быстрее переходить на ретранслятор в сетях, где прямое соединение невозможно. Оставьте пустым для значения по умолчанию 2.5 секунды."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Sa rechesta de cumpartzidura de sa schermada in su dispositivu remotu est acabada chene si cumpletare"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk no at pòdidu carrigare unu cumponente de GStreamer netzessàriu pro registrare sa schermada ({})"),
("Relay fallback delay in seconds", "Tardu prima de impreare su relè in segundos"),
("relay-fallback-delay-tip", "Cantu tempus una connessione de relè giai istabilida abetat sa connessione direta WebRTC prima de èssere impreada in su postu suo. Aumenta pro dare prus tempus a una connessione direta lenta; diminuì pro colare prima a su relè in sas retes in ue non si podet fàghere una connessione direta. Lassa bòidu pro su valore predefinidu de 2.5 segundos."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Žiadosť o zdieľanie obrazovky na vzdialenom zariadení sa skončila bez dokončenia"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nedokázal načítať komponent GStreamera potrebný na zachytenie obrazovky ({})"),
("Relay fallback delay in seconds", "Oneskorenie pred prepnutím na prenos v sekundách"),
("relay-fallback-delay-tip", "Ako dlho už nadviazané spojenie cez prenos čaká na priame spojenie WebRTC, kým sa použije namiesto neho. Zvýšte, aby pomalé priame spojenie malo viac času; znížte, aby sa v sieťach, kde priame spojenie nie je možné, skôr prešlo na prenos. Nechajte prázdne pre predvolenú hodnotu 2.5 sekundy."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Zahteva za skupno rabo zaslona na oddaljeni napravi se je končala, ne da bi bila dokončana"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ni mogel naložiti komponente GStreamer, potrebne za zajem zaslona ({})"),
("Relay fallback delay in seconds", "Zakasnitev pred preklopom na posrednika v sekundah"),
("relay-fallback-delay-tip", "Kako dolgo že vzpostavljena posredniška povezava čaka na neposredno povezavo WebRTC, preden se uporabi namesto nje. Povečajte, da počasni neposredni povezavi date več časa; zmanjšajte, da v omrežjih, kjer neposredna povezava ni mogoča, hitreje preklopite na posrednika. Pustite prazno za privzeto vrednost 2.5 sekunde."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Kërkesa për ndarjen e ekranit në pajisjen e largët përfundoi pa u kryer"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nuk mundi të ngarkojë një komponent të GStreamer të nevojshëm për regjistrimin e ekranit ({})"),
("Relay fallback delay in seconds", "Vonesa para kalimit te releja në sekonda"),
("relay-fallback-delay-tip", "Sa gjatë pret një lidhje releje tashmë e vendosur lidhjen e drejtpërdrejtë WebRTC përpara se të përdoret në vend të saj. Rriteni për t'i dhënë më shumë kohë një lidhjeje të drejtpërdrejtë të ngadaltë; uleni për të kaluar më shpejt te releja në rrjete ku lidhja e drejtpërdrejtë nuk është e mundur. Lëreni bosh për vlerën e parazgjedhur prej 2.5 sekondash."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Zahtev za deljenje ekrana na udaljenom uređaju završio se bez dovršetka"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk nije mogao da učita GStreamer komponentu potrebnu za snimanje ekrana ({})"),
("Relay fallback delay in seconds", "Кашњење пре преласка на релеј у секундама"),
("relay-fallback-delay-tip", "Колико дуго већ успостављена релејна веза чека на директну WebRTC везу пре него што се употреби уместо ње. Повећајте да бисте спорој директној вези дали више времена; смањите да бисте на мрежама где директна веза није могућа брже прешли на релеј. Оставите празно за подразумевану вредност од 2.5 секунде."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Begäran om skärmdelning på fjärrenheten avslutades utan att slutföras"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk kunde inte läsa in en GStreamer-komponent som krävs för skärminspelning ({})"),
("Relay fallback delay in seconds", "Fördröjning innan relä används i sekunder"),
("relay-fallback-delay-tip", "Hur länge en redan upprättad reläanslutning väntar på den direkta WebRTC-anslutningen innan den används i stället. Öka värdet för att ge en långsam direktanslutning mer tid; sänk det för att snabbare falla tillbaka på reläet i nätverk där direktanslutning inte är möjlig. Lämna tomt för standardvärdet 2.5 sekunder."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "தொலைநிலை சாதனத்தில் திரை பகிர்வு கோரிக்கை நிறைவடையாமல் முடிந்தது"), ("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 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 ஏற்ற முடியவில்லை ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "திரைப் பதிவுக்குத் தேவையான GStreamer கூறை RustDesk ஏற்ற முடியவில்லை ({})"),
("Relay fallback delay in seconds", "ரிலேக்கு மாறுவதற்கான தாமதம் வினாடிகளில்"),
("relay-fallback-delay-tip", "ஏற்கனவே நிறுவப்பட்ட ரிலே இணைப்பு நேரடி WebRTC இணைப்புக்காக எவ்வளவு நேரம் காத்திருக்கிறது, அதன் பிறகு அதற்குப் பதிலாகப் பயன்படுத்தப்படுகிறது. மெதுவான நேரடி இணைப்புக்கு அதிக நேரம் வழங்க அதிகரிக்கவும்; நேரடி இணைப்பு சாத்தியமில்லாத பிணையங்களில் விரைவாக ரிலேக்கு மாற குறைக்கவும். இயல்புநிலை 2.5 வினாடிகளுக்கு காலியாக விடவும்."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", ""), ("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 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 ({})", ""), ("RustDesk could not load a GStreamer component needed for screen capture ({})", ""),
("Relay fallback delay in seconds", ""),
("relay-fallback-delay-tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "คำขอแชร์หน้าจอบนอุปกรณ์ระยะไกลสิ้นสุดลงโดยไม่เสร็จสมบูรณ์"), ("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 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 ที่จำเป็นสำหรับการบันทึกหน้าจอได้ ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ไม่สามารถโหลดส่วนประกอบ GStreamer ที่จำเป็นสำหรับการบันทึกหน้าจอได้ ({})"),
("Relay fallback delay in seconds", "เวลารอก่อนเปลี่ยนไปใช้รีเลย์ (วินาที)"),
("relay-fallback-delay-tip", "การเชื่อมต่อผ่านรีเลย์ที่สร้างไว้แล้วจะรอการเชื่อมต่อ WebRTC โดยตรงนานเท่าใดก่อนที่จะถูกใช้แทน เพิ่มค่าเพื่อให้การเชื่อมต่อโดยตรงที่ช้ามีเวลามากขึ้น ลดค่าเพื่อเปลี่ยนไปใช้รีเลย์เร็วขึ้นในเครือข่ายที่ไม่สามารถเชื่อมต่อโดยตรงได้ เว้นว่างไว้เพื่อใช้ค่าเริ่มต้น 2.5 วินาที"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Uzak cihazdaki ekran paylaşımı isteği tamamlanmadan sona erdi"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk ekran yakalama için gereken GStreamer bileşenini yükleyemedi ({})"),
("Relay fallback delay in seconds", "Aktarıcıya geçiş gecikmesi (saniye)"),
("relay-fallback-delay-tip", "Zaten kurulmuş bir aktarıcı bağlantısının, onun yerine kullanılmadan önce doğrudan WebRTC bağlantısını ne kadar beklediğidir. Yavaş bir doğrudan bağlantıya daha fazla süre tanımak için artırın; doğrudan bağlantının kurulamadığı ağlarda aktarıcıya daha erken geçmek için azaltın. Varsayılan 2.5 saniye için boş bırakın."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "遠端裝置上的螢幕分享要求已結束,但未完成"), ("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 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 元件 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk 無法載入螢幕擷取所需的 GStreamer 元件 ({})"),
("Relay fallback delay in seconds", "回退到中繼前的等待時間(秒)"),
("relay-fallback-delay-tip", "已經建立的中繼連線會等待直連的 WebRTC 多久,超過這個時間就改用中繼。調大可以讓較慢的直連有更多機會勝出;調小則在無法直連的網路上更快回退到中繼。留空表示使用預設值 2.5 秒。"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Запит на демонстрацію екрана на віддаленому пристрої завершився, не будучи виконаним"), ("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 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, потрібний для захоплення екрана ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не вдалося завантажити компонент GStreamer, потрібний для захоплення екрана ({})"),
("Relay fallback delay in seconds", "Затримка перед переходом на ретранслятор у секундах"),
("relay-fallback-delay-tip", "Скільки часу вже встановлене з'єднання через ретранслятор чекає на пряме з'єднання WebRTC, перш ніж буде використане замість нього. Збільште, щоб дати повільному прямому з'єднанню більше часу; зменште, щоб швидше переходити на ретранслятор у мережах, де пряме з'єднання неможливе. Залиште порожнім для типового значення 2.5 секунди."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,6 +778,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "ریموٹ ڈیوائس پر اسکرین شیئرنگ کی درخواست مکمل ہوئے بغیر ختم ہو گئی"), ("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 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 جزو لوڈ نہیں کر سکا ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk اسکرین ریکارڈنگ کے لیے درکار GStreamer جزو لوڈ نہیں کر سکا ({})"),
("Relay fallback delay in seconds", "ریلے پر واپس جانے میں تاخیر سیکنڈ میں"),
("relay-fallback-delay-tip", "پہلے سے قائم ریلے کنکشن براہ راست WebRTC کنکشن کا کتنی دیر انتظار کرتا ہے، اس کے بعد اس کی جگہ استعمال ہوتا ہے۔ سست براہ راست کنکشن کو مزید وقت دینے کے لیے بڑھائیں؛ ان نیٹ ورکس پر جہاں براہ راست کنکشن ممکن نہیں، جلد ریلے پر جانے کے لیے کم کریں۔ پہلے سے طے شدہ 2.5 سیکنڈ کے لیے خالی چھوڑ دیں۔"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -778,5 +778,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("The screen sharing request ended without completing on the remote device", "Yêu cầu chia sẻ màn hình trên thiết bị từ xa đã kết thúc mà chưa hoàn tất"), ("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 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 ({})"), ("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk không thể tải một thành phần GStreamer cần cho việc ghi màn hình ({})"),
("Relay fallback delay in seconds", "Độ trễ trước khi chuyển sang trung chuyển (giây)"),
("relay-fallback-delay-tip", "Kết nối trung chuyển đã thiết lập sẽ chờ kết nối WebRTC trực tiếp trong bao lâu trước khi được dùng thay thế. Tăng giá trị để cho kết nối trực tiếp chậm thêm thời gian; giảm để chuyển sang trung chuyển sớm hơn trên các mạng không thể kết nối trực tiếp. Để trống để dùng giá trị mặc định 2.5 giây."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

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

View File

@@ -15,7 +15,9 @@
use super::*; use super::*;
#[cfg(not(any(target_os = "linux", target_os = "android")))] #[cfg(not(any(target_os = "linux", target_os = "android")))]
use hbb_common::anyhow::anyhow; use hbb_common::anyhow::anyhow;
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}; use std::sync::atomic::{AtomicBool, Ordering};
pub const NAME: &'static str = "audio"; pub const NAME: &'static str = "audio";
@@ -97,10 +99,11 @@ mod pa_impl {
RESTARTING.store(false, Ordering::SeqCst); RESTARTING.store(false, Ordering::SeqCst);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let mut stream = crate::ipc::connect(1000, "_pa").await?; let mut stream = crate::ipc::connect(1000, "_pa").await?;
unsafe { let mut encoder = AudioEncoder::new(Encoder::new(
AUDIO_ZERO_COUNT = 0; crate::platform::PA_SAMPLE_RATE,
} Stereo,
let mut encoder = Encoder::new(crate::platform::PA_SAMPLE_RATE, Stereo, LowDelay)?; LowDelay,
)?);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
allow_err!( allow_err!(
stream stream
@@ -172,8 +175,11 @@ pub fn is_screen_capture_kit_available() -> bool {
} }
#[cfg(not(any(target_os = "linux", target_os = "android")))] #[cfg(not(any(target_os = "linux", target_os = "android")))]
#[path = "audio_capture_error.rs"] mod audio_capture;
#[cfg(not(any(target_os = "linux", target_os = "android")))]
mod audio_capture_error; mod audio_capture_error;
#[cfg(not(any(target_os = "linux", target_os = "android")))]
mod audio_capture_queue;
#[cfg(not(any(target_os = "linux", target_os = "android")))] #[cfg(not(any(target_os = "linux", target_os = "android")))]
mod cpal_impl { mod cpal_impl {
@@ -182,14 +188,15 @@ mod cpal_impl {
use super::*; use super::*;
use cpal::{ use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait}, traits::{DeviceTrait, HostTrait, StreamTrait},
BufferSize, Device, Host, InputCallbackInfo, StreamConfig, SupportedStreamConfig, Device, Host, InputCallbackInfo, SupportedStreamConfig,
}; };
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref HOST: Host = cpal::default_host(); static ref HOST: Host = cpal::default_host();
static ref INPUT_BUFFER: Arc<Mutex<std::collections::VecDeque<f32>>> = Default::default();
} }
const AUDIO_PACKETS_PER_SECOND: usize = 100;
#[cfg(feature = "screencapturekit")] #[cfg(feature = "screencapturekit")]
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref HOST_SCREEN_CAPTURE_KIT: Result<Host, cpal::HostUnavailable> = cpal::host_from_id(cpal::HostId::ScreenCaptureKit); static ref HOST_SCREEN_CAPTURE_KIT: Result<Host, cpal::HostUnavailable> = cpal::host_from_id(cpal::HostId::ScreenCaptureKit);
@@ -197,7 +204,20 @@ mod cpal_impl {
#[derive(Default)] #[derive(Default)]
pub struct State { pub struct State {
stream: Option<(Box<dyn StreamTrait>, Arc<Message>, CaptureErrorHandler)>, 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 { impl super::service::Reset for State {
@@ -215,8 +235,8 @@ mod cpal_impl {
} }
_ => {} _ => {}
} }
if let Some((_, format, _)) = &state.stream { if let Some(stream) = &state.stream {
sp.send_shared(format.clone()); sp.send_shared(stream.format.clone());
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
log::info!("Audio capture stream recreated; replacement format sent"); log::info!("Audio capture stream recreated; replacement format sent");
} }
@@ -232,8 +252,8 @@ mod cpal_impl {
} }
_ => {} _ => {}
} }
if let Some((_, format, _)) = &state.stream { if let Some(stream) = &state.stream {
sps.send_shared(format.clone()); sps.send_shared(stream.format.clone());
} }
Ok(()) Ok(())
})?; })?;
@@ -241,10 +261,10 @@ mod cpal_impl {
} }
pub fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { pub fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> {
if let Some((_, _, errors)) = &state.stream { if let Some(stream) = &state.stream {
if errors.needs_restart() { if stream.errors.needs_restart() {
// Recreate on the service thread, outside the backend's error callback. // Recreate on the service thread, outside the capture callbacks.
log::warn!("Recreating interrupted audio capture stream"); log::warn!("Recreating audio capture stream after an error");
super::restart(); super::restart();
} }
} }
@@ -255,29 +275,89 @@ mod cpal_impl {
} }
} }
fn send( #[derive(Clone, Copy)]
data: Vec<f32>, struct CaptureFrameProcessorConfig {
sample_rate0: u32, input_rate: u32,
sample_rate: u32, output_rate: u32,
device_channel: u16, device_channel: u16,
encode_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);
} }
if device_channel != encode_channel {
data = crate::common::audio_rechannel( struct CaptureFrameProcessor {
data, config: CaptureFrameProcessorConfig,
sample_rate, resampler: Option<crate::audio_resampler::FixedFrameAudioResampler>,
sample_rate, sender: audio_capture_queue::CapturePcmSender,
device_channel, rechannel_buffer: Vec<f32>,
encode_channel, }
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,
),
})
}
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);
} }
send_f32(&data, encoder, sp); Ok(())
}
}
fn capture_packet_layout(sample_rate: u32, channels: u16) -> ResultType<(usize, usize)> {
if sample_rate < AUDIO_PACKETS_PER_SECOND as u32 || channels == 0 {
bail!("Invalid audio capture layout: sample_rate={sample_rate}, channels={channels}");
}
let frames = sample_rate as usize / AUDIO_PACKETS_PER_SECOND;
let samples = frames.checked_mul(channels as usize).with_context(|| {
format!(
"Audio capture frame size overflow: sample_rate={sample_rate}, channels={channels}"
)
})?;
Ok((frames, samples))
} }
#[cfg(feature = "screencapturekit")] #[cfg(feature = "screencapturekit")]
@@ -367,9 +447,7 @@ mod cpal_impl {
Ok((device, format)) Ok((device, format))
} }
fn play( fn play(sp: &GenericService) -> ResultType<ActiveCaptureStream> {
sp: &GenericService,
) -> ResultType<(Box<dyn StreamTrait>, Arc<Message>, CaptureErrorHandler)> {
use cpal::SampleFormat::*; use cpal::SampleFormat::*;
let (device, config) = get_device()?; let (device, config) = get_device()?;
let sp = sp.clone(); let sp = sp.clone();
@@ -387,109 +465,274 @@ mod cpal_impl {
48000 48000
}; };
let ch = if config.channels() > 1 { Stereo } else { Mono }; let ch = if config.channels() > 1 { Stereo } else { Mono };
let max_channels = config.channels().max(ch as u16);
let (_, 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() { let (stream, errors) = match config.sample_format() {
I8 => build_input_stream::<i8>(device, &config, sp, sample_rate, ch)?, I8 => build_input_stream::<i8>(device, &config, output)?,
I16 => build_input_stream::<i16>(device, &config, sp, sample_rate, ch)?, I16 => build_input_stream::<i16>(device, &config, output)?,
I32 => build_input_stream::<i32>(device, &config, sp, sample_rate, ch)?, I32 => build_input_stream::<i32>(device, &config, output)?,
I64 => build_input_stream::<i64>(device, &config, sp, sample_rate, ch)?, I64 => build_input_stream::<i64>(device, &config, output)?,
U8 => build_input_stream::<u8>(device, &config, sp, sample_rate, ch)?, U8 => build_input_stream::<u8>(device, &config, output)?,
U16 => build_input_stream::<u16>(device, &config, sp, sample_rate, ch)?, U16 => build_input_stream::<u16>(device, &config, output)?,
U32 => build_input_stream::<u32>(device, &config, sp, sample_rate, ch)?, U32 => build_input_stream::<u32>(device, &config, output)?,
U64 => build_input_stream::<u64>(device, &config, sp, sample_rate, ch)?, U64 => build_input_stream::<u64>(device, &config, output)?,
F32 => build_input_stream::<f32>(device, &config, sp, sample_rate, ch)?, F32 => build_input_stream::<f32>(device, &config, output)?,
F64 => build_input_stream::<f64>(device, &config, sp, sample_rate, ch)?, F64 => build_input_stream::<f64>(device, &config, output)?,
f => bail!("unsupported audio format: {:?}", f), f => bail!("unsupported audio format: {:?}", f),
}; };
stream.play()?; stream.play()?;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
log::info!("Audio capture start call succeeded"); log::info!("Audio capture start call succeeded");
Ok(( Ok(ActiveCaptureStream {
Box::new(stream), stream: Some(Box::new(stream)),
Arc::new(create_format_msg(sample_rate, ch as _)), format: Arc::new(create_format_msg(sample_rate, ch as _)),
_encoder_worker: encoder_worker,
errors, errors,
)) })
}
fn convert_input_samples<T>(data: &[T]) -> impl Iterator<Item = f32> + '_
where
T: cpal::SizedSample,
f32: cpal::FromSample<T>,
{
data.iter()
.map(|sample| <f32 as cpal::FromSample<T>>::from_sample_(*sample))
}
#[cfg(target_os = "macos")]
fn log_capture_startup<T>(
data: &[T],
received_samples: bool,
received_signal: bool,
) -> (bool, bool)
where
T: cpal::SizedSample,
f32: cpal::FromSample<T>,
{
// Starting capture does not guarantee sample delivery or audible data.
if !received_samples && !data.is_empty() {
log::info!(
"Audio capture received first PCM block: {} samples",
data.len()
);
}
let has_signal = received_signal
|| convert_input_samples(data).any(|sample| sample.is_finite() && sample != 0.0);
if !received_signal && has_signal {
log::info!("Audio capture received first nonzero PCM");
}
(received_samples || !data.is_empty(), has_signal)
} }
fn build_input_stream<T>( fn build_input_stream<T>(
device: cpal::Device, device: cpal::Device,
config: &cpal::SupportedStreamConfig, config: &cpal::SupportedStreamConfig,
sp: GenericService, output: CaptureStreamOutput,
sample_rate: u32,
encode_channel: magnum_opus::Channels,
) -> ResultType<(cpal::Stream, CaptureErrorHandler)> ) -> ResultType<(cpal::Stream, CaptureErrorHandler)>
where where
T: cpal::SizedSample + dasp::sample::ToSample<f32>, T: cpal::SizedSample,
f32: cpal::FromSample<T>,
{ {
let errors = CaptureErrorHandler::default(); let errors = CaptureErrorHandler::default();
let callback_errors = errors.clone(); let callback_errors = errors.clone();
let err_fn = move |err| callback_errors.handle(err); let err_fn = move |err| callback_errors.handle(err);
let processor_errors = errors.clone();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let (mut received_samples, mut received_signal) = (false, false); let (mut received_samples, mut received_signal) = (false, false);
let sample_rate_0 = config.sample_rate().0; let sample_rate_0 = config.sample_rate().0;
log::debug!("Audio sample rate : {}", sample_rate); log::debug!("Audio sample rate : {}", output.sample_rate);
unsafe {
AUDIO_ZERO_COUNT = 0;
}
let device_channel = config.channels(); let device_channel = config.channels();
let mut encoder = Encoder::new(sample_rate, encode_channel, LowDelay)?; let (_, capture_frame_samples) = capture_packet_layout(sample_rate_0, device_channel)?;
// https://www.opus-codec.org/docs/html_api/group__opusencoder.html#gace941e4ef26ed844879fde342ffbe546 let mut frame = audio_capture::CaptureFrameBuffer::new(capture_frame_samples)?;
// https://chromium.googlesource.com/chromium/deps/opus/+/1.1.1/include/opus.h let processor_config = CaptureFrameProcessorConfig {
// Do not set `frame_size = sample_rate as usize / 100;` input_rate: sample_rate_0,
// Because we find `sample_rate as usize / 100` will cause encoder error in `encoder.encode_vec_float()` sometimes. output_rate: output.sample_rate,
// https://github.com/xiph/opus/blob/2554a89e02c7fc30a980b4f7e635ceae1ecba5d6/src/opus_encoder.c#L725 device_channel,
let frame_size = sample_rate_0 as usize / 100; // 10 ms encode_channel: output.encode_channel as _,
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 mut processor = CaptureFrameProcessor::new(processor_config, output.sender)?;
let timeout = None;
let stream = device.build_input_stream( let stream = device.build_input_stream(
&stream_config, &config.config(),
move |data: &[T], _: &InputCallbackInfo| { move |data: &[T], _: &InputCallbackInfo| {
let buffer: Vec<f32> = data.iter().map(|s| T::to_sample(*s)).collect(); if processor_errors.needs_restart() {
return;
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
// Starting capture does not guarantee sample delivery or audible data. (received_samples, received_signal) =
if !received_samples && !buffer.is_empty() { log_capture_startup(data, received_samples, received_signal);
received_samples = true;
log::info!(
"Audio capture received first PCM block: {} samples",
buffer.len()
);
}
if !received_signal
&& buffer
.iter()
.any(|sample| sample.is_finite() && *sample != 0.0)
{
received_signal = true;
log::info!("Audio capture received first nonzero PCM");
}
}
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,
);
} }
frame.process(convert_input_samples(data), |frame| {
processor_errors.process_frame(|| processor.process(frame));
});
}, },
err_fn, err_fn,
timeout, timeout,
)?; )?;
Ok((stream, errors)) Ok((stream, 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);
}
}
} }
fn create_format_msg(sample_rate: u32, channels: u16) -> Message { fn create_format_msg(sample_rate: u32, channels: u16) -> Message {
@@ -505,29 +748,44 @@ fn create_format_msg(sample_rate: u32, channels: u16) -> Message {
msg 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 // every audio data length is set to 480
// MAX_AUDIO_ZERO_COUNT=800 is similar as Gate Attack Time 3~5s(Linux) || 6~8s(Windows) // MAX_AUDIO_ZERO_COUNT=800 is similar as Gate Attack Time 3~5s(Linux) || 6~8s(Windows)
const MAX_AUDIO_ZERO_COUNT: u16 = 800; const MAX_AUDIO_ZERO_COUNT: u16 = 800;
static mut AUDIO_ZERO_COUNT: u16 = 0;
fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { struct AudioEncoder {
encoder: Encoder,
zero_count: u16,
}
impl AudioEncoder {
fn new(encoder: Encoder) -> Self {
Self {
encoder,
zero_count: 0,
}
}
fn should_encode(&mut self, data: &[f32]) -> bool {
if data.iter().filter(|x| **x != 0.).next().is_some() { if data.iter().filter(|x| **x != 0.).next().is_some() {
unsafe { self.zero_count = 0;
AUDIO_ZERO_COUNT = 0; } else if self.zero_count > MAX_AUDIO_ZERO_COUNT {
} if self.zero_count == MAX_AUDIO_ZERO_COUNT + 1 {
} 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"); log::debug!("Audio Zero Gate Attack");
AUDIO_ZERO_COUNT += 1; self.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; return;
} }
AUDIO_ZERO_COUNT += 1;
}
}
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ {
// the permitted opus data size are 120, 240, 480, 960, 1920, and 2880 // the permitted opus data size are 120, 240, 480, 960, 1920, and 2880
@@ -539,6 +797,7 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) {
let n = input_size / BATCH_SIZE; let n = input_size / BATCH_SIZE;
for i in 0..n { for i in 0..n {
match encoder match encoder
.encoder
.encode_vec_float(&data[i * BATCH_SIZE..(i + 1) * BATCH_SIZE], BATCH_SIZE) .encode_vec_float(&data[i * BATCH_SIZE..(i + 1) * BATCH_SIZE], BATCH_SIZE)
{ {
Ok(data) => { Ok(data) => {
@@ -549,7 +808,7 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) {
}); });
sp.send(msg_out); sp.send(msg_out);
} }
Err(_) => {} Err(error) => log::warn!("Failed to encode audio frame: {error:?}"),
} }
} }
} else { } else {
@@ -559,7 +818,7 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) {
} }
#[cfg(not(target_os = "android"))] #[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) => { Ok(data) => {
let mut msg_out = Message::new(); let mut msg_out = Message::new();
msg_out.set_audio_frame(AudioFrame { msg_out.set_audio_frame(AudioFrame {
@@ -568,6 +827,6 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) {
}); });
sp.send(msg_out); 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

@@ -11,7 +11,10 @@ pub(super) struct CaptureErrorHandler {
impl CaptureErrorHandler { impl CaptureErrorHandler {
pub(super) fn handle(&self, error: cpal::StreamError) { pub(super) fn handle(&self, error: cpal::StreamError) {
if matches!(error, cpal::StreamError::StreamInterrupted { .. }) { if matches!(
error,
cpal::StreamError::StreamInterrupted { .. } | cpal::StreamError::DeviceNotAvailable
) {
// ScreenCaptureKit can stop capture while the remote session stays open. // ScreenCaptureKit can stop capture while the remote session stays open.
// The observed -3821 error does not identify its underlying trigger. // The observed -3821 error does not identify its underlying trigger.
// https://developer.apple.com/documentation/screencapturekit/scstreamdelegate/stream(_:didstopwitherror:) // https://developer.apple.com/documentation/screencapturekit/scstreamdelegate/stream(_:didstopwitherror:)
@@ -23,6 +26,20 @@ impl CaptureErrorHandler {
} }
} }
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 { pub(super) fn needs_restart(&self) -> bool {
self.interrupted.load(Ordering::Relaxed) self.interrupted.load(Ordering::Relaxed)
} }
@@ -69,4 +86,44 @@ mod tests {
}); });
assert!(!errors.needs_restart()); 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

@@ -571,7 +571,7 @@ impl VideoQoS {
adjust_ratio = user.delay.fps.is_none(); adjust_ratio = user.delay.fps.is_none();
user.delay.fps = Some(fps); user.delay.fps = Some(fps);
let base = user.delay.rtt_calculator.get_rtt().unwrap_or_default(); let base = user.delay.rtt_calculator.get_rtt().unwrap_or_default();
log::debug!( 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}", "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(), hbb_common::get_time(),
delay.saturating_sub(base), delay.saturating_sub(base),