Compare commits

..

1 Commits

Author SHA1 Message Date
rustdesk
7e1c45c370 fix(audio): three 100% CPU busy loops on the _pa path
The Linux audio service in `--server` ignored the `Err` from `next_raw()`, so
once the cm-side `_pa` peer closed, every iteration re-polled a dead socket:
tokio-util's paused `Framed` issues one 0-byte read per poll and returns ready
at once, never `Pending`. The thread never parked and burned a full core for
the life of the process. Propagate instead, so `ServiceTmpl::run`'s existing
backoff ends the inner loop and reconnects.

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:23:29 +08:00
22 changed files with 258 additions and 935 deletions

45
Cargo.lock generated
View File

@@ -1717,7 +1717,7 @@ dependencies = [
[[package]]
name = "cpal"
version = "0.15.3"
source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#96d4da121b7d949677ac5b6887413a9185fd7f39"
source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#69ad2578adc9200093fc81cdfbdad63dbc4274f9"
dependencies = [
"alsa",
"cidre",
@@ -4121,7 +4121,8 @@ dependencies = [
[[package]]
name = "interceptor"
version = "0.14.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac0781c825d602095113772e389ef0607afcb869ae0e68a590d8e0799cdcef8"
dependencies = [
"async-trait",
"bytes",
@@ -7007,7 +7008,8 @@ dependencies = [
[[package]]
name = "rtcp"
version = "0.13.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9689528bf3a9eb311fd938d05516dd546412f9ce4fffc8acfc1db27cc3dbf72"
dependencies = [
"bytes",
"thiserror 1.0.61",
@@ -7017,7 +7019,8 @@ dependencies = [
[[package]]
name = "rtp"
version = "0.13.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c54733451a67d76caf9caa07a7a2cec6871ea9dda92a7847f98063d459200f4b"
dependencies = [
"bytes",
"memchr",
@@ -7462,7 +7465,8 @@ dependencies = [
[[package]]
name = "sdp"
version = "0.8.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd277015eada44a0bb810a4b84d3bf6e810573fa62fb442f457edf6a1087a69"
dependencies = [
"rand 0.8.5",
"substring",
@@ -8034,7 +8038,8 @@ dependencies = [
[[package]]
name = "stun"
version = "0.8.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dbc2bab375524093c143dc362a03fb6a1fb79e938391cdb21665688f88a088a"
dependencies = [
"base64 0.22.1",
"crc",
@@ -8916,7 +8921,8 @@ dependencies = [
[[package]]
name = "turn"
version = "0.10.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f5aea1116456e1da71c45586b87c72e3b43164fbf435eb93ff6aa475416a9a4"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -9579,7 +9585,8 @@ dependencies = [
[[package]]
name = "webrtc"
version = "0.13.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24bab7195998d605c862772f90a452ba655b90a2f463c850ac032038890e367a"
dependencies = [
"arc-swap",
"async-trait",
@@ -9622,7 +9629,8 @@ dependencies = [
[[package]]
name = "webrtc-data"
version = "0.11.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e97b932854da633a767eff0cc805425a2222fc6481e96f463e57b015d949d1d"
dependencies = [
"bytes",
"log",
@@ -9636,7 +9644,8 @@ dependencies = [
[[package]]
name = "webrtc-dtls"
version = "0.12.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ccbe4d9049390ab52695c3646c1395c877e16c15fb05d3bda8eee0c7351711c"
dependencies = [
"aes",
"aes-gcm",
@@ -9672,7 +9681,8 @@ dependencies = [
[[package]]
name = "webrtc-ice"
version = "0.13.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb51bde0d790f109a15bfe4d04f1b56fb51d567da231643cb3f21bb74d678997"
dependencies = [
"arc-swap",
"async-trait",
@@ -9696,7 +9706,8 @@ dependencies = [
[[package]]
name = "webrtc-mdns"
version = "0.9.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "979cc85259c53b7b620803509d10d35e2546fa505d228850cbe3f08765ea6ea8"
dependencies = [
"log",
"socket2 0.5.10",
@@ -9708,7 +9719,8 @@ dependencies = [
[[package]]
name = "webrtc-media"
version = "0.10.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80041211deccda758a3e19aa93d6b10bc1d37c9183b519054b40a83691d13810"
dependencies = [
"byteorder",
"bytes",
@@ -9720,7 +9732,7 @@ dependencies = [
[[package]]
name = "webrtc-sctp"
version = "0.12.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "git+https://github.com/rustdesk-org/webrtc?rev=db3b07a9dd8f195916c89c2e62a8911402b11d27#db3b07a9dd8f195916c89c2e62a8911402b11d27"
dependencies = [
"arc-swap",
"async-trait",
@@ -9737,7 +9749,8 @@ dependencies = [
[[package]]
name = "webrtc-srtp"
version = "0.15.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01e773f79b09b057ffbda6b03fe7b43403b012a240cf8d05d630674c3723b5bb"
dependencies = [
"aead",
"aes",
@@ -9759,7 +9772,7 @@ dependencies = [
[[package]]
name = "webrtc-util"
version = "0.11.0"
source = "git+https://github.com/rustdesk-org/webrtc?rev=80d5a20532cf58f5d4d237c437a98ceb85ee40dc#80d5a20532cf58f5d4d237c437a98ceb85ee40dc"
source = "git+https://github.com/rustdesk-org/webrtc?rev=db3b07a9dd8f195916c89c2e62a8911402b11d27#db3b07a9dd8f195916c89c2e62a8911402b11d27"
dependencies = [
"async-trait",
"bitflags 1.3.2",

View File

@@ -231,11 +231,8 @@ libxdo-sys = { path = "libs/libxdo-sys-stub" }
# the SACK settle the rest (F-RTO), timed from the latest send, so a stall no longer resends the
# whole backlog behind itself while a short lost tail still comes back at once.
# Pinned by rev, not branch: a fork branch can be rewritten out from under the lockfile.
# webrtc: SettingEngine cannot reach the ICE agent's max_binding_requests, which decides how
# long the answerer keeps checking a pair that has not answered yet.
webrtc = { git = "https://github.com/rustdesk-org/webrtc", rev = "80d5a20532cf58f5d4d237c437a98ceb85ee40dc" }
webrtc-util = { git = "https://github.com/rustdesk-org/webrtc", rev = "80d5a20532cf58f5d4d237c437a98ceb85ee40dc" }
webrtc-sctp = { git = "https://github.com/rustdesk-org/webrtc", rev = "80d5a20532cf58f5d4d237c437a98ceb85ee40dc" }
webrtc-util = { git = "https://github.com/rustdesk-org/webrtc", rev = "db3b07a9dd8f195916c89c2e62a8911402b11d27" }
webrtc-sctp = { git = "https://github.com/rustdesk-org/webrtc", rev = "db3b07a9dd8f195916c89c2e62a8911402b11d27" }
[package.metadata.winres]
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."

View File

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

View File

@@ -2443,6 +2443,9 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t
return NULL;
}
// to-do: use `fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI`.
// We keep `fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI` for compatibility.
// fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI;
fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI;
fd->dwFileAttributes = GetFileAttributesW(file_name);
if (fd->dwFileAttributes == INVALID_FILE_ATTRIBUTES)
@@ -2455,34 +2458,7 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t
fd->dwFlags &= ~FD_WRITESTIME;
}
// Announce the size in the file list. Without FD_FILESIZE the receiving side cannot
// trust the size fields, so CliprdrStream_New() asks for each file's size with its own
// FILECONTENTS_SIZE request and blocks on the reply. Those requests are made for every
// entry up front, while the shell is inside IDataObject::GetData(), so the cost grows
// with the number of files and not with their size.
//
// GetFileSize() reports failure as INVALID_FILE_SIZE, which cannot be told apart from a
// genuine 4GB-1 file without GetLastError(), and it fails outright on the directory
// handles FILE_FLAG_BACKUP_SEMANTICS lets us open above. A directory gets no size. A
// file whose size cannot be read is rejected rather than sent with the flag off: the
// Unix receiver reads the size fields whether or not the flag is set, for compatibility
// with older Windows senders, and would take the zero for an empty file.
if ((fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0)
{
LARGE_INTEGER file_size = {0};
if (!GetFileSizeEx(hFile, &file_size) || file_size.QuadPart < 0)
{
CloseHandle(hFile);
free(fd);
return NULL;
}
fd->nFileSizeLow = file_size.LowPart;
fd->nFileSizeHigh = (DWORD)file_size.HighPart;
fd->dwFlags |= FD_FILESIZE;
}
fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh);
if ((wcslen(file_name + pathLen) + 1) > sizeof(fd->cFileName) / sizeof(fd->cFileName[0]))
{
// The file name is too long, which is not a normal case.
@@ -3539,24 +3515,15 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
{
if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE)
{
FILEDESCRIPTORW *fd;
if (clipboard->nFiles <= fileContentsRequest->listIndex)
{
rc = ERROR_INTERNAL_ERROR;
goto exit;
}
fd = clipboard->fileDescriptor[fileContentsRequest->listIndex];
// The size fields only mean anything when FD_FILESIZE says so. Answering with
// them regardless would describe an entry whose size could not be read as empty.
if ((fd->dwFlags & FD_FILESIZE) == 0)
{
rc = ERROR_INTERNAL_ERROR;
goto exit;
}
*((UINT32 *)&pData[0]) = fd->nFileSizeLow;
*((UINT32 *)&pData[4]) = fd->nFileSizeHigh;
*((UINT32 *)&pData[0]) =
clipboard->fileDescriptor[fileContentsRequest->listIndex]->nFileSizeLow;
*((UINT32 *)&pData[4]) =
clipboard->fileDescriptor[fileContentsRequest->listIndex]->nFileSizeHigh;
uSize = cbRequested;
}
else if (fileContentsRequest->dwFlags == FILECONTENTS_RANGE)

View File

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

View File

@@ -95,10 +95,7 @@ pub use super::lang::*;
#[cfg(not(target_os = "linux"))]
mod audio_playback;
#[cfg(target_os = "windows")]
mod audio_playback_recovery;
#[cfg(all(test, not(target_os = "linux")))]
#[path = "client/tests/audio_state_tests.rs"]
mod audio_state_tests;
pub mod file_trait;
pub mod helper;
@@ -2086,8 +2083,6 @@ pub struct AudioHandler {
device_channel: u16,
#[cfg(not(target_os = "linux"))]
playback_status: Arc<audio_playback::AudioPlaybackStatus>,
#[cfg(target_os = "windows")]
playback_recovery: audio_playback_recovery::PlaybackRecovery,
}
#[cfg(not(target_os = "linux"))]
@@ -2395,53 +2390,22 @@ impl AudioHandler {
/// Handle audio format and create an audio decoder.
pub fn handle_format(&mut self, f: AudioFormat) {
self.handle_format_with_start(f, Self::start_audio);
}
fn handle_format_with_start(
&mut self,
f: AudioFormat,
start: impl FnOnce(&mut Self, AudioFormat) -> ResultType<()>,
) {
if !is_supported_audio_channel_count(f.channels) {
log::error!("Unsupported audio channel count: {}", f.channels);
return;
}
match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) {
Ok(d) => {
#[cfg(target_os = "windows")]
let playback_failed = self.cancel_pending_playback();
#[cfg(target_os = "linux")]
let keep_existing_stream = self.simple.is_some()
&& self.sample_rate.0 == f.sample_rate
&& u32::from(self.channels) == f.channels;
#[cfg(not(target_os = "linux"))]
let keep_existing_stream = self.audio_stream.is_some()
&& self.sample_rate.0 == f.sample_rate
&& u32::from(self.channels) == f.channels;
let keep_existing_stream = false;
let buffer = vec![0.; f.sample_rate as usize * f.channels as usize];
#[cfg(not(target_os = "linux"))]
let mut previous = std::mem::take(self);
#[cfg(target_os = "windows")]
self.prepare_playback(&f);
self.audio_decoder = Some((d, buffer));
self.channels = f.channels as _;
let result = start(self, f);
#[cfg(target_os = "windows")]
let keep_existing_stream = keep_existing_stream
&& !playback_failed
&& !previous.playback_recovery.report_pending();
#[cfg(not(target_os = "linux"))]
if result.is_err() && keep_existing_stream {
// The restarted capture has new Opus history even when output startup fails.
previous.audio_decoder = self.audio_decoder.take();
*self = previous;
self.handle_audio_start_result(result, true);
return;
}
#[cfg(target_os = "windows")]
self.finish_playback_replacement(result, keep_existing_stream.then_some(previous));
#[cfg(not(target_os = "windows"))]
let result = self.start_audio(f);
self.handle_audio_start_result(result, keep_existing_stream);
}
Err(err) => {
@@ -2529,9 +2493,6 @@ impl AudioHandler {
device: &Device,
) -> ResultType<()> {
self.device_channel = config.channels;
#[cfg(target_os = "windows")]
let err_fn = self.playback_recovery.new_error_callback();
#[cfg(not(target_os = "windows"))]
let err_fn = move |err| {
// too many errors, will improve later
log::trace!("an error occurred on stream: {}", err);
@@ -4104,11 +4065,7 @@ pub fn start_audio_thread() -> MediaSender {
std::thread::spawn(move || {
let mut audio_handler = AudioHandler::default();
loop {
#[cfg(target_os = "windows")]
let received = audio_handler.receive_audio(&audio_receiver);
#[cfg(not(target_os = "windows"))]
let received = audio_receiver.recv();
if let Ok(data) = received {
if let Ok(data) = audio_receiver.recv() {
match data {
MediaData::AudioFrame(af) => {
audio_handler.handle_frame(*af);

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -780,6 +780,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk non è riuscito a caricare un componente GStreamer necessario per l'acquisizione dello schermo ({})"),
("Relay fallback delay in seconds", "Ritardo fallback relay (secondi)"),
("relay-fallback-delay-tip", "Quanto tempo una connessione relay già attiva attende la connessione WebRTC diretta prima di essere usata. Aumentalo per dare a una connessione diretta lenta più tempo per funzionare; diminuiscilo per passare prima al relay sulle reti in cui non è possibile effettuare una connessione diretta. Lascia vuoto per il valore predefinito di 2,5 secondi."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "Per avviare una chiamata vocale, attiva nella pagina 'Condivisione schermo' la voce 'Cattura audio'.")
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -780,6 +780,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk가 화면 캡처에 필요한 GStreamer 구성 요소를 불러오지 못했습니다 ({})"),
("Relay fallback delay in seconds", "릴레이 대체 작동 지연 시간 (초)"),
("relay-fallback-delay-tip", "이미 연결된 중계 연결이 직접 WebRTC 연결을 얼마나 기다린 후 대신 사용되는지입니다. 값을 늘리면 느린 직접 연결에 더 많은 시간을 주고, 줄이면 직접 연결이 불가능한 네트워크에서 더 빨리 중계로 전환합니다. 비워 두면 기본값 2.5초가 사용됩니다."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "음성 통화를 시작하려면 '화면 공유' 페이지에서 '오디오 캡처'를 사용함으로 하세요.")
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Click to upgrade", "Iniciar atualização"),
("Configure", "Configurar"),
("config_acc", "Para controlar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Acessibilidade\"."),
("config_screen", "Para acessar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\""),
("config_screen", "Para acessar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\"/"),
("Installing ...", "Instalando ..."),
("Install", "Instalar"),
("Installation", "Instalação"),
@@ -764,7 +764,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Sync clipboard between sessions", "Sincronizar área de transferência entre sessões"),
("sync-clipboard-between-sessions-tip", "Texto ou imagens copiados em uma sessão remota também são enviados para a área de transferência das suas outras sessões conectadas."),
("terminal-clipboard-write-tip", "Aplicativos do terminal podem copiar para a área de transferência"),
("Allow terminal apps to copy to clipboard", "Permitir cópia do terminal para a área de transferência"),
("Allow terminal apps to copy to clipboard", "Permitir que aplicativos do terminal copiem para a área de transferência"),
("Enable", "Habilitar"),
("Reuse one connection for port forwarding", "Reutilizar uma conexão para encaminhamento de portas"),
("port-forward-mux-tip", "Levar todas as conexões de um encaminhamento de portas por uma única conexão com o outro computador, em vez de estabelecer uma nova conexão e fazer login novamente para cada uma."),
@@ -779,7 +779,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("RustDesk could not obtain a usable screen from the XDG Desktop Portal, the PipeWire library may be too old", "O RustDesk não conseguiu obter uma tela utilizável do XDG Desktop Portal. A biblioteca do PipeWire pode estar desatualizada."),
("RustDesk could not load a GStreamer component needed for screen capture ({})", "O RustDesk não conseguiu carregar um componente do GStreamer necessário para a captura de tela ({})."),
("Relay fallback delay in seconds", "Atraso antes de recorrer ao retransmissor em segundos"),
("relay-fallback-delay-tip", "Tempo que a conexão de retransmissão aguarda pela conexão direta WebRTC. Aumente para dar mais tempo a conexões lentas; diminua para usar o retransmissor mais cedo. Deixe vazio para usar o padrão de 2,5 segundos."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "Para iniciar uma chamada de voz, ative \"Captura de áudio\" na página \"Compartilhamento de tela\".")
("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."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -730,7 +730,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("preset-password-in-use-tip", "Установленный пароль сейчас используется."),
("Enable privacy mode", "Использовать режим конфиденциальности"),
("allow-remote-toolbar-docking-any-edge", "Разрешать прикрепление удалённой панели инструментов к любому краю окна"),
("API Token", "Токен API"),
("API Token", "API-токен"),
("Deploy", "Развернуть"),
("Custom ID (optional)", "Пользовательский ID (необязательно)"),
("server_requires_deployment_tip", "Сервер требует явного развёртывания этого устройства. Развернуть сейчас?"),
@@ -748,23 +748,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show on the minimized toolbar", "Показывать на свёрнутой панели инструментов"),
("All monitors", "Все мониторы"),
("#{} monitor", "Монитор {}"),
("conn-e2ee-unavailable-tip", "Невозможно проверить сквозное шифрование.\nУдалённое устройство, возможно, ещё настраивается. Повторите попытку позже.\nЕсли это повторяется, сервер может быть ненадёжным.\nВсё равно продолжить?"),
("conn-e2ee-unavailable-tip", "Не удалось проверить сквозное шифрование.\nУдаленное устройство, возможно, еще настраивается. Повторите попытку позже.\nЕсли это повторяется, сервер может быть ненадежным.\nВсе равно продолжить?"),
("ID whitelisting", "Список разрешённых ID"),
("Use ID whitelisting", "Использовать белый список ID"),
("id_whitelist_tip", "Только ID из белого списка могут получить доступ к моему устройству."),
("id_whitelist_wildcard_tip", "Поддерживаются подстановочные знаки: \"*\" соответствует любому количеству символов, \"?\" — ровно одному символу"),
("id_whitelist_wildcard_tip", "Поддерживаются подстановочные знаки: '*' соответствует любому количеству символов, '?' — ровно одному символу"),
("Invalid ID", "Неправильный ID"),
("Your ID is blocked by the peer", "Ваш ID заблокирован удалённым устройством"),
("Your ip is blocked by the peer", "Ваш IP-адрес заблокирован удалённым устройством"),
("id_whitelist_caveat_tip", "ID сообщается подключающимся клиентом. Белый список уменьшает поверхность атаки и не заменяет пароль или 2FA"),
("whitelist_cidr_tip", "Поддерживается нотация CIDR, например: 192.168.1.0/24"),
("whitelist_cidr_tip", "Поддерживается нотация CIDR, например 192.168.1.0/24"),
("Continue", "Продолжить"),
("Browser didn't open? Use the url below to sign in.", "Браузер не открылся? Используйте ссылку ниже для входа."),
("Lock canvas", "Заблокировать холст"),
("Sync clipboard between sessions", "Синхронизировать буфер обмена между сеансами"),
("sync-clipboard-between-sessions-tip", "Текст или изображения, скопированные в одном удалённом сеансе, также отправляются в буфер обмена других подключённых сеансов."),
("terminal-clipboard-write-tip", "Приложение в терминале хочет скопировать текст в буфер обмена этого устройства. Если соответствующее разрешение предоставлено, оно применяется к приложениям в терминале во всех соединениях, пока вы не отключите его в настройках. Ручные копирование и вставка не затрагиваются."),
("Allow terminal apps to copy to clipboard", "Разрешить приложениям в терминале копирование в буфер обмена"),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Включить"),
("Reuse one connection for port forwarding", "Использовать одно подключение для перенаправления портов"),
("port-forward-mux-tip", "Передавать все соединения одного перенаправления портов через одно подключение к удалённому устройству вместо повторного подключения и входа для каждого из них."),
@@ -780,6 +780,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("RustDesk could not load a GStreamer component needed for screen capture ({})", "RustDesk не удалось загрузить компонент GStreamer, необходимый для захвата экрана ({})"),
("Relay fallback delay in seconds", "Задержка перед переходом на ретранслятор в секундах"),
("relay-fallback-delay-tip", "Сколько времени уже установленное соединение через ретранслятор ждёт прямое соединение WebRTC, прежде чем будет использовано вместо него. Увеличьте, чтобы дать медленному прямому соединению больше времени; уменьшите, чтобы быстрее переходить на ретранслятор в сетях, где прямое соединение невозможно. Оставьте пустым для значения по умолчанию 2.5 секунды."),
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "Чтобы начать голосовой вызов, включите \"Захват аудио\" на странице \"Демонстрация экрана\" настроек.")
("To start a voice call, enable \"Audio capture\" on the \"Screen share\" page.", "")
].iter().cloned().collect();
}

View File

@@ -542,26 +542,6 @@ extern "C"
SHAddToRecentDocs(SHARD_PATHW, path);
}
// Hyper-V Enhanced Session names do not have the usual "rdp" prefix.
static bool is_rdp_session_by_protocol(DWORD session_id)
{
LPSTR buffer = nullptr;
DWORD bytes = 0;
if (!WTSQuerySessionInformationA(
WTS_CURRENT_SERVER_HANDLE, session_id, WTSClientProtocolType, &buffer, &bytes)) {
flog("Failed to query protocol for session %lu: Windows error %lu\n",
session_id, GetLastError());
return false;
}
std::unique_ptr<char, decltype(&WTSFreeMemory)> protocol_info(buffer, WTSFreeMemory);
if (!buffer || bytes < sizeof(USHORT)) {
flog("Failed to query protocol for session %lu: Windows error %lu\n",
session_id, static_cast<DWORD>(ERROR_INVALID_DATA));
return false;
}
return *reinterpret_cast<const USHORT *>(buffer) == WTS_PROTOCOL_TYPE_RDP;
}
DWORD get_current_session(BOOL include_rdp)
{
auto rdp_or_console = WTSGetActiveConsoleSessionId();
@@ -593,10 +573,6 @@ extern "C"
{
rdp_or_console = info.SessionId;
}
else if (is_rdp_session_by_protocol(info.SessionId))
{
rdp_or_console = info.SessionId;
}
}
}
WTSFreeMemory(pInfos);
@@ -690,9 +666,6 @@ extern "C"
else if (include_rdp && !strnicmp(info.pWinStationName, ica, nica)) {
sessionIds.push_back(std::wstring(L"ICA:") + std::to_wstring(info.SessionId));
}
else if (include_rdp && is_rdp_session_by_protocol(info.SessionId)) {
sessionIds.push_back(std::wstring(L"RDP:") + std::to_wstring(info.SessionId));
}
}
}
WTSFreeMemory(pInfos);

View File

@@ -391,15 +391,6 @@ mod cpal_impl {
if !audio_input.is_empty() {
return get_audio_input(&audio_input);
}
// The pinned CPAL uses event-driven WASAPI loopback here. Windows versions
// before Windows 10 1703 do not signal capture events, so system audio does
// not work on Win7. #16095 kept the same CPAL revision and loopback path;
// this limitation predates that PR.
// Ordinary microphone input is supported on Win7 and uses the branch above.
// #16095 added its callback-to-encoder wake dependency; see CapturePcmSender::wake
// for the new scheduling risk, whose audible impact on Win7 is unmeasured.
// https://learn.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording
// https://learn.microsoft.com/en-us/windows/win32/coreaudio/capturesharedeventdriven
let device = HOST
.default_output_device()
.with_context(|| "Failed to get default output device for loopback")?;

View File

@@ -97,7 +97,6 @@ impl Drop for CaptureEncoderWorker {
fn drop(&mut self) {
self.stop.store(true, Ordering::Release);
if let Some(handle) = self.handle.take() {
// Owner-thread shutdown already waits via join(); see CapturePcmSender::wake for Win7.
handle.thread().unpark();
if let Err(error) = handle.join() {
log::error!("Failed to join audio encoder thread: {error:?}");
@@ -210,16 +209,6 @@ impl CapturePcmSender {
fn wake(&self) {
if let Some(thread) = self.handoff.wake_thread.get() {
// #16095 moved Opus encoding and message submission from the capture callback to a worker.
// Previously, the callback did that work directly, with allocations and blocking locks.
// On Win7 with Rust 1.75, if the worker is descheduled after publishing PARKED but before
// NtWaitForKeyedEvent, unpark() waits in NtReleaseKeyedEvent until the worker enters that wait.
// It does not wait for encoding; park_timeout() does not bound the callback's wait.
// Delays can cause gaps or stall teardown; a Win7 microphone regression has not been measured.
// System loopback already failed on Win7 before #16095 (see cpal_impl::get_device),
// so the affected path is microphone/input-device capture, including outgoing voice calls.
// Accept this risk to preserve Win7 input capture without a separate legacy notifier.
// https://github.com/rust-lang/rust/blob/1.75.0/library/std/src/sys/windows/thread_parking.rs
thread.unpark();
}
}

View File

@@ -299,6 +299,12 @@ pub struct Connection {
tx_input: std_mpsc::Sender<MessageInput>,
// handle input messages
video_ack_required: bool,
// Diagnostics only, gated by `RUSTDESK_QOS_VERBOSE`: how long the shared
// write path blocked this second. The video send is inline in the message
// loop, so a slow write also delays the delay probe and its reply.
video_send_max_ms: u32,
video_send_sum_ms: u32,
video_send_count: u32,
server_audit_conn: String,
server_audit_file: String,
controlled_context: Option<ControlledContext>,
@@ -504,6 +510,9 @@ impl Connection {
show_my_cursor: false,
tx_input,
video_ack_required: false,
video_send_max_ms: 0,
video_send_sum_ms: 0,
video_send_count: 0,
server_audit_conn: "".to_owned(),
server_audit_file: "".to_owned(),
controlled_context,
@@ -950,10 +959,17 @@ impl Connection {
video_service::notify_video_frame_fetched(vf.display as usize, id, Some(instant.into()));
}
}
let send_begin = video_service::qos_diag_verbose().then(Instant::now);
if let Err(err) = conn.stream.send(&value as &Message).await {
conn.on_close(&err.to_string(), false).await;
break;
}
if let Some(begin) = send_begin {
let blocked = begin.elapsed().as_millis() as u32;
conn.video_send_max_ms = conn.video_send_max_ms.max(blocked);
conn.video_send_sum_ms = conn.video_send_sum_ms.saturating_add(blocked);
conn.video_send_count += 1;
}
},
Some((instant, value)) = rx.recv() => {
let latency = instant.elapsed().as_millis() as i64;
@@ -1040,6 +1056,21 @@ impl Connection {
break;
}
}
if video_service::qos_diag_verbose() && conn.video_send_count > 0 {
// Joined with `qos_trace` on `t`: a probe that waits behind a
// blocked write is not a slow network.
log::debug!(
"qos_send t={} id={id} frames={} send_max={} send_sum={} queued={}",
hbb_common::get_time(),
conn.video_send_count,
conn.video_send_max_ms,
conn.video_send_sum_ms,
rx_video.len()
);
conn.video_send_max_ms = 0;
conn.video_send_sum_ms = 0;
conn.video_send_count = 0;
}
conn.file_remove_log_control.on_timer().drain(..).map(|x| conn.send_to_cm(x)).count();
#[cfg(feature = "hwcodec")]
conn.update_supported_encoding();

View File

@@ -625,7 +625,7 @@ impl VideoQoS {
.clamp(MIN_AUTO_FPS.min(user_cap), user_cap)
.min(current);
user.delay.fps = Some(fps);
log::trace!(
log::debug!(
"qos_trace t={} id={id} timeout={elapsed} fps={fps}",
hbb_common::get_time()
);

View File

@@ -52,8 +52,6 @@ use scrap::{
CodecFormat, Display, EncodeInput, TraitCapturer, TraitPixelBuffer,
};
#[cfg(windows)]
use std::io::ErrorKind::ConnectionReset;
#[cfg(windows)]
use std::sync::Once;
use std::{
collections::HashSet,
@@ -64,59 +62,6 @@ use std::{
pub const OPTION_REFRESH: &'static str = "refresh";
#[cfg(windows)]
const DXGI_RECOVERY_LIMIT: usize = 3;
#[cfg(windows)]
const DXGI_RECOVERY_WINDOW: Duration = Duration::from_secs(10);
#[cfg(windows)]
const DXGI_RECOVERY_FRAME_GRACE: Duration = Duration::from_secs(2);
#[cfg(windows)]
struct DxgiRecoveryState {
attempts: usize,
window_started: Option<Instant>,
restart_pending: bool,
fallback_pending: bool,
}
#[cfg(windows)]
impl DxgiRecoveryState {
fn new() -> Self {
Self {
attempts: 0,
window_started: None,
restart_pending: false,
fallback_pending: false,
}
}
fn next_attempt(&mut self) -> Option<usize> {
if self
.window_started
.map(|started| started.elapsed() > DXGI_RECOVERY_WINDOW)
.unwrap_or(true)
{
self.attempts = 0;
self.window_started = Some(Instant::now());
}
if self.attempts >= DXGI_RECOVERY_LIMIT {
self.fallback_pending = true;
return None;
}
self.attempts += 1;
self.restart_pending = true;
Some(self.attempts)
}
fn take_restart_pending(&mut self) -> bool {
std::mem::take(&mut self.restart_pending)
}
fn take_fallback_pending(&mut self) -> bool {
std::mem::take(&mut self.fallback_pending)
}
}
type FrameFetchedNotifierSender = UnboundedSender<(i32, Option<Instant>)>;
type FrameFetchedNotifierReceiver = Arc<TokioMutex<UnboundedReceiver<(i32, Option<Instant>)>>>;
@@ -275,8 +220,6 @@ pub struct VideoService {
sp: GenericService,
idx: usize,
source: VideoSource,
#[cfg(windows)]
dxgi_recovery_state: Arc<Mutex<DxgiRecoveryState>>,
}
impl Deref for VideoService {
@@ -310,8 +253,6 @@ pub fn new(source: VideoSource, idx: usize) -> GenericService {
sp: GenericService::new(get_service_name(source, idx), true),
idx,
source,
#[cfg(windows)]
dxgi_recovery_state: Arc::new(Mutex::new(DxgiRecoveryState::new())),
};
GenericService::run(&vs, run);
vs.sp
@@ -624,25 +565,9 @@ fn run(vs: VideoService) -> ResultType<()> {
let last_portable_service_running = false;
let display_idx = vs.idx;
#[cfg(windows)]
let dxgi_recovery_state = vs.dxgi_recovery_state.clone();
let sp = vs.sp;
let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?;
#[cfg(windows)]
// ACCESS_LOST marks the next successful capturer creation as a recovery. This timestamp is
// consumed once and temporarily holds off the normal WouldBlock-to-GDI fallback, giving the
// replacement DXGI capturer time to produce its first frame. Normal startup is unaffected.
let dxgi_recovery_started = dxgi_recovery_state
.lock()
.unwrap()
.take_restart_pending()
.then(Instant::now);
#[cfg(windows)]
if dxgi_recovery_state.lock().unwrap().take_fallback_pending() {
c.set_gdi();
log::info!("dxgi recovery exhausted, fall back to gdi");
}
#[cfg(windows)]
if !scrap::codec::enable_directx_capture() && !c.is_gdi() {
log::info!("disable dxgi with option, fall back to gdi");
c.set_gdi();
@@ -730,6 +655,12 @@ fn run(vs: VideoService) -> ResultType<()> {
let capture_width = c.width;
let capture_height = c.height;
let (mut second_instant, mut send_counter) = (Instant::now(), 0);
// Diagnostics only. `send_counter` counts capture rounds, which is not the
// number of frames that reached a connection: the encoder's own rate control
// drops frames when the bitrate cannot carry them. `wait_max_ms` is how long
// a round waited for the previous frame to be picked up, so a blocked write
// shows up here as capture stalling rather than as a slow network.
let (mut sent_counter, mut wait_max_ms) = (0usize, 0u32);
while sp.ok() {
#[cfg(windows)]
@@ -740,6 +671,8 @@ fn run(vs: VideoService) -> ResultType<()> {
&mut spf,
client_record,
&mut send_counter,
&mut sent_counter,
&mut wait_max_ms,
&mut second_instant,
&sp.name(),
)?;
@@ -860,6 +793,9 @@ fn run(vs: VideoService) -> ResultType<()> {
capture_width,
capture_height,
)?;
if !send_conn_ids.is_empty() {
sent_counter += 1;
}
frame_controller.set_send(now, send_conn_ids);
send_counter += 1;
}
@@ -879,18 +815,13 @@ fn run(vs: VideoService) -> ResultType<()> {
match res {
Err(ref e) if e.kind() == WouldBlock => {
#[cfg(windows)]
if dxgi_recovery_started
.map(|started| started.elapsed() >= DXGI_RECOVERY_FRAME_GRACE)
.unwrap_or(true)
{
if try_gdi > 0 && !c.is_gdi() {
if try_gdi > 3 {
c.set_gdi();
try_gdi = 0;
log::info!("No image, fall back to gdi");
}
try_gdi += 1;
if try_gdi > 0 && !c.is_gdi() {
if try_gdi > 3 {
c.set_gdi();
try_gdi = 0;
log::info!("No image, fall back to gdi");
}
try_gdi += 1;
}
#[cfg(target_os = "linux")]
{
@@ -924,19 +855,15 @@ fn run(vs: VideoService) -> ResultType<()> {
capture_width,
capture_height,
)?;
if !send_conn_ids.is_empty() {
sent_counter += 1;
}
frame_controller.set_send(now, send_conn_ids);
send_counter += 1;
}
}
}
Err(err) => {
#[cfg(windows)]
// The display-change check can restart capture before error handling below.
let recovery_attempt = if !c.is_gdi() && err.kind() == ConnectionReset {
dxgi_recovery_state.lock().unwrap().next_attempt()
} else {
None
};
// This check may be redundant, but it is better to be safe.
// The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough.
if vs.source.is_monitor() {
@@ -945,19 +872,6 @@ fn run(vs: VideoService) -> ResultType<()> {
#[cfg(windows)]
if !c.is_gdi() {
if err.kind() == ConnectionReset {
if let Some(attempt) = recovery_attempt {
log::debug!(
"dxgi access lost, restart capture: attempt {attempt}, error: {err:?}"
);
bail!("SWITCH");
}
log::warn!(
"dxgi access lost after {DXGI_RECOVERY_LIMIT} restarts in {} seconds, fall back to gdi: {err:?}",
DXGI_RECOVERY_WINDOW.as_secs()
);
dxgi_recovery_state.lock().unwrap().take_fallback_pending();
}
c.set_gdi();
log::info!("dxgi error, fall back to gdi: {:?}", err);
continue;
@@ -985,6 +899,7 @@ fn run(vs: VideoService) -> ResultType<()> {
break;
}
}
wait_max_ms = wait_max_ms.max(wait_begin.elapsed().as_millis() as u32);
DISPLAY_CONN_IDS.lock().unwrap().remove(&display_idx);
let elapsed = now.elapsed();
@@ -1415,12 +1330,22 @@ pub fn make_display_changed_msg(
Some(msg_out)
}
/// Per-second pipeline diagnostics, off unless `RUSTDESK_QOS_VERBOSE` is set.
/// The default log level is `debug`, so an unconditional line here would land in
/// every user's log file once a second forever. Nothing enables it implicitly.
pub(crate) fn qos_diag_verbose() -> bool {
static VERBOSE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*VERBOSE.get_or_init(|| std::env::var("RUSTDESK_QOS_VERBOSE").is_ok())
}
fn check_qos(
encoder: &mut Encoder,
ratio: &mut f32,
spf: &mut Duration,
client_record: bool,
send_counter: &mut usize,
sent_counter: &mut usize,
wait_max_ms: &mut u32,
second_instant: &mut Instant,
name: &str,
) -> ResultType<()> {
@@ -1446,7 +1371,21 @@ fn check_qos(
if second_instant.elapsed() > Duration::from_secs(1) {
*second_instant = Instant::now();
video_qos.update_display_data(&name, *send_counter);
// Diagnostics only, joined with `qos_trace` on `t`: the controller's target
// is not the rate the encoder produced, and neither is the rate the send
// path accepted.
if qos_diag_verbose() {
log::debug!(
"qos_video t={} display={name} captured={} sent={} wait_max={}",
hbb_common::get_time(),
*send_counter,
*sent_counter,
*wait_max_ms
);
}
*send_counter = 0;
*sent_counter = 0;
*wait_max_ms = 0;
}
drop(video_qos);
Ok(())