mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-16 09:21:01 +03:00
Compare commits
1 Commits
clipboard-
...
fix-16226-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e1c45c370 |
45
Cargo.lock
generated
45
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Submodule libs/hbb_common updated: daf84c2df3...29cf7cbe4d
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
113
src/client/audio_state_tests.rs
Normal file
113
src/client/audio_state_tests.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user