Compare commits

..

20 Commits

Author SHA1 Message Date
21pages
e17efa9474 fix svt-av1 build on windows, benchmark
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-08-05 23:52:13 +08:00
rustdesk
8eda19247c feat(codec): log which AV1 encoder a session uses
Emit an explicit "AV1 encoder: svt-av1" / "AV1 encoder: aom" line at the
point the encoder is actually built, plus the reason ("i444" /
"unsupported resolution") when a 64-bit host falls back to aom for AV1.
Avoids relying on parsing Debug output or SVT's own stderr banner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wf7v1KJM6mbhiYxWSz4PH
2026-07-23 17:52:15 +08:00
rustdesk
1ac684db0d feat(codec): use SVT-AV1 for AV1 encoding on 64-bit targets
Encode AV1 with SVT-AV1 v4.2.0 (rtc + low delay + CBR) instead of libaom
on all 64-bit targets; 32-bit targets keep aom. Decoding is unchanged and
the wire format is still AV1, so old peers are unaffected.

- vcpkg overlay port pinned to v4.2.0: static build, LTO off (mixed
  toolchain safety), MSVC >= 1950 inline workaround, no forced llvm
  binutils
- low delay contract: one packet out per picture in, blocking get_packet
- bitrate and frame rate change on the fly via RATE_CHANGE_EVENT /
  FRAME_RATE_CHANGE_EVENT; VideoQoS pushes its pacing rate through the
  new EncoderApi::set_fps, applied only after a successful send
- fall back to aom for i444 (svt-av1 is 4:2:0 only), for unsupported
  resolutions and on encoder init failure
- test_av1 now gates auto AV1 on the encoder actually used, the cached
  verdict is re-keyed (SVT-Y/SVT-N) so upgrades re-measure
- unit tests: per-frame packet contract, aom cross-decode of the
  bitstream, on-the-fly event acceptance

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wf7v1KJM6mbhiYxWSz4PH
2026-07-23 01:50:57 +08:00
hatterp
1c2dd71891 Translate 'Continue' to 'Kontynuuj' in Polish (#15641) 2026-07-21 22:43:09 +08:00
Kuksgauzen
5b4d6baf47 fix: wrap BackingScaleFactor in autoreleasepool to stop NSDictionary accumulation on macOS (#15623)
Signed-off-by: Viktor Kuksgauzen <vkpiar@gmail.com>
2026-07-21 05:42:43 +08:00
gateslu
7696b0ee51 fix(linux): forward forced display server to user server (#15627)
Signed-off-by: Gateslu <lyjbbq@163.com>
2026-07-20 13:24:48 +08:00
CHarris
20ab5ab0ad fix(deploy): don't wipe local id when --deploy gets an empty --id (#15633)
`rustdesk --deploy --id ""` (e.g. an unset variable in a deployment
script) deploys a blank id, then wipes the local id and unconfirms the
key through the IPC config write. The Android deploy flow already guards
an empty id (#15146); apply the same guard to the CLI, and reject an
empty id at the IPC write boundary the same way the read path was fixed
in #15626.
2026-07-20 10:58:28 +08:00
CHarris
c01300be20 fix(ipc): never adopt an empty id from the main IPC (#15626) 2026-07-20 10:22:46 +08:00
cui fliter
5f015c9da1 Translate Continue into Simplified Chinese (#15621)
Signed-off-by: cuishuang <imcusg@gmail.com>
2026-07-18 17:56:27 +08:00
RustDesk
082a5a2a4e Revert "Fix Adjust Window sizing across DPI (#15592)" (#15620)
This reverts commit 61f0944990.
2026-07-18 16:37:30 +08:00
21pages
61f0944990 Fix Adjust Window sizing across DPI (#15592)
* Fix Adjust Window sizing across DPI and fullscreen transitions

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

* Use visible screen frame for Adjust Window

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

* Tolerate floating-point errors in Adjust Window sizing

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

* Fix review, fix Adjust Window async metric guards

Capture the current screen before awaiting window geometry so one target-frame calculation uses consistent screen metrics.

Return early when adjusting without a context and the Flutter view list is empty, instead of calling views.first after the window or engine may have been torn
    down.

Clarify the platform coordinate units used for Adjust Window scaling.

* Fix Adjust Window for maximized Linux windows

Unmaximize Linux remote windows before applying Adjust Window because native setFrame may be ignored while the window is maximized.

* Fix Adjust Window screen refresh guards

Refresh screen metrics before checking Adjust Window availability and again after exiting fullscreen so target-frame calculation uses current window geometry.

Hide Adjust Window on web because resizing relies on desktop window APIs.

* Fix review, handle missing window frame in Adjust Window

Return null when WindowController.getFrame fails so Adjust Window availability checks and resize attempts skip cleanly if the window is hidden or disposed.

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-07-18 16:32:58 +08:00
21pages
96e2a330b8 restrict switch sides to remote desktop sessions (#15610)
* fix: restrict switch sides to remote desktop sessions

 Reject switch sides requests outside authenticated remote desktop sessions, and reject switch sides responses that try to carry non-remote login types.

 Add scope coverage so file transfer, terminal, view camera, and port forward sessions cannot use switch sides.

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

* fix review: consume switch sides UUID before rejecting response

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-07-17 15:52:49 +08:00
21pages
5abf4e9724 Fix disabled installation bypass (#15598)
* Fix disabled installation bypass

Prevent install.exe and --install from opening the install flow when disable-installation is set.

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

* Refine disabled installation handling for portable clients

Document why --install must be filtered from both Rust and Flutter runner arguments for portable wrappers such as no-install.exe. Remove redundant UI-
    layer installation checks because the install entry points are already gated upstream.

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-07-16 16:00:38 +08:00
jinqiang zhang
cf2b28faf9 fix linux llvm22 build (#15565)
bindgen-0.65 is incompatible with llvm 22, we should upgrade to a
newer bindgen version

error message:

```
error[E0609]: no field `g_w` on type `vpx_codec_enc_cfg`
  --> libs/scrap/src/common/vpxcodec.rs:66:19
   |
66 |                 c.g_w = config.width;
   |                   ^^^ unknown field
   |
   = note: available field is: `_address`
```
2026-07-15 13:27:15 +08:00
fufesou
bdb38c4730 fix: check valid id (#15535)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-07-14 15:35:52 +08:00
Maison da Silva
fa418cace6 Translate 'Continue' to 'Continuar' in ptbr.rs (#15567)
Translate 'Continue' to 'Continuar' in ptbr.rs
2026-07-13 18:56:41 +08:00
rustdesk
94a2a2bb4a fix https://github.com/rustdesk/rustdesk/issues/15566 2026-07-11 21:43:04 +08:00
Maison da Silva
865fe71c46 Update Portuguese translations for clarity (#15534) 2026-07-11 17:18:12 +08:00
rustdesk
137298e05a revert back 2026-07-10 15:21:48 +08:00
rustdesk
685a89a171 target Android 15 2026-07-10 15:12:06 +08:00
28 changed files with 2108 additions and 72 deletions

32
Cargo.lock generated
View File

@@ -771,6 +771,26 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags 2.9.1",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"log",
"prettyplease",
"proc-macro2 1.0.93",
"quote 1.0.36",
"regex",
"rustc-hash 2.1.1",
"shlex",
"syn 2.0.98",
]
[[package]]
name = "bit_field"
version = "0.10.2"
@@ -2329,7 +2349,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.7.4",
"libloading 0.8.4",
]
[[package]]
@@ -2694,7 +2714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4494,7 +4514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
dependencies = [
"cfg-if 1.0.0",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -7434,7 +7454,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -7491,7 +7511,7 @@ dependencies = [
"security-framework 3.5.1",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -7578,7 +7598,7 @@ name = "scrap"
version = "0.5.0"
dependencies = [
"android_logger",
"bindgen 0.65.1",
"bindgen 0.72.1",
"block",
"cfg-if 1.0.0",
"dbus",

View File

@@ -495,14 +495,14 @@ class _CmHeaderState extends State<_CmHeader>
if (client.type_() == ClientType.file)
FittedBox(
child: Text(
translate("File Transfer"),
translate("Transfer file"),
style: TextStyle(color: Colors.white70, fontSize: 12),
),
),
if (client.type_() == ClientType.camera)
FittedBox(
child: Text(
translate("View Camera"),
translate("View camera"),
style: TextStyle(color: Colors.white70, fontSize: 12),
),
),

View File

@@ -14,6 +14,7 @@
typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*);
typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int);
typedef int (*FUNC_RUSTDESK_GET_APP_NAME)(wchar_t*, int);
typedef int (*FUNC_RUSTDESK_IS_DISABLE_INSTALLATION)();
/// Note: `--server`, `--service` are already handled in [core_main.rs].
const std::vector<std::string> parameters_white_list = {"--install", "--cm"};
@@ -62,6 +63,22 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
}
std::vector<std::string> rust_args(c_args, c_args + args_len);
free_c_args(c_args, args_len);
FUNC_RUSTDESK_IS_DISABLE_INSTALLATION rustdesk_is_disable_installation =
(FUNC_RUSTDESK_IS_DISABLE_INSTALLATION)GetProcAddress(hInstance, "rustdesk_is_disable_installation");
bool is_disable_installation =
rustdesk_is_disable_installation && rustdesk_is_disable_installation() != 0;
const auto installParam = std::string("--install");
// Flutter reads the original process command line, not only rust_args, so
// remove the `--install` injected by the portable wrapper here as well. This
// also lets `no-install.exe` continue as a portable app when installation is
// disabled. See: https://github.com/rustdesk/rustdesk-server-pro/issues/991#issuecomment-4978376890
if (is_disable_installation) {
command_line_arguments.erase(
std::remove(command_line_arguments.begin(),
command_line_arguments.end(),
installParam),
command_line_arguments.end());
}
std::wstring app_name = L"RustDesk";
FUNC_RUSTDESK_GET_APP_NAME get_rustdesk_app_name = (FUNC_RUSTDESK_GET_APP_NAME)GetProcAddress(hInstance, "get_rustdesk_app_name");
@@ -118,7 +135,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
is_cm_page = true;
}
bool is_install_page = false;
auto installParam = std::string("--install");
if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, installParam.size(), installParam.c_str()) == 0) {
is_install_page = true;
}

View File

@@ -48,7 +48,7 @@ quest = "0.3"
[build-dependencies]
target_build_utils = "0.3"
bindgen = "0.65"
bindgen = "0.72.1"
pkg-config = { version = "0.3.27", optional = true }
[target.'cfg(target_os = "linux")'.dependencies]

View File

@@ -4,19 +4,48 @@ use std::{
println,
};
/// The static library name may differ from the package name.
fn link_lib_name(package: &str) -> &str {
match package {
"svt-av1" => "SvtAv1Enc",
_ => package.trim_start_matches("lib"),
}
}
#[cfg(all(target_os = "linux", feature = "linux-pkg-config"))]
fn link_pkg_config(name: &str) -> Vec<PathBuf> {
// sometimes an override is needed
let pc_name = match name {
"libvpx" => "vpx",
"svt-av1" => "SvtAv1Enc",
_ => name,
};
let lib = pkg_config::probe_library(pc_name)
let mut pc = pkg_config::Config::new();
if name == "svt-av1" {
// svt_av1.rs requires the 4.x API (rtc, PredStructure, on-the-fly rate events)
pc.atleast_version("4.2");
}
let pkg_hint = match name {
"svt-av1" => "libsvt-av1-dev (needs svt-av1 >= 4.2)".to_owned(),
_ => format!("{pc_name}-dev"),
};
let lib = pc.probe(pc_name)
.expect(format!(
"unable to find '{pc_name}' development headers with pkg-config (feature linux-pkg-config is enabled).
try installing '{pc_name}-dev' from your system package manager.").as_str());
try installing '{pkg_hint}' from your system package manager.").as_str());
lib.include_paths
let mut include_paths = lib.include_paths;
// SvtAv1Enc.pc's Cflags is -I${includedir}/svt-av1, but svt_av1_ffi.h uses
// #include <svt-av1/...>, so the parent include root is needed too.
if name == "svt-av1" {
let parents: Vec<PathBuf> = include_paths
.iter()
.filter(|p| p.ends_with("svt-av1"))
.filter_map(|p| p.parent().map(|p| p.to_path_buf()))
.collect();
include_paths.extend(parents);
}
include_paths
}
#[cfg(not(all(target_os = "linux", feature = "linux-pkg-config")))]
fn link_pkg_config(_name: &str) -> Vec<PathBuf> {
@@ -61,10 +90,7 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf {
path.push("installed");
}
path.push(target);
println!(
"cargo:rustc-link-lib=static={}",
name.trim_start_matches("lib")
);
println!("cargo:rustc-link-lib=static={}", link_lib_name(name));
println!(
"cargo:rustc-link-search={}",
path.join("lib").to_str().unwrap()
@@ -103,11 +129,13 @@ fn link_homebrew_m1(name: &str) -> PathBuf {
);
}
path.push(directories.pop().unwrap());
// Link the library.
println!(
"cargo:rustc-link-lib=static={}",
name.trim_start_matches("lib")
);
// Link the library. Homebrew's svt-av1 bottle only ships a dylib.
if name == "svt-av1" {
println!("cargo:rustc-link-lib={}", link_lib_name(name));
println!("cargo:warning=svt-av1 is linked dynamically from homebrew, the binary needs homebrew's svt-av1 at runtime and is not relocatable; set VCPKG_ROOT for a static build");
} else {
println!("cargo:rustc-link-lib=static={}", link_lib_name(name));
}
// Add the library path.
println!(
"cargo:rustc-link-search={}",
@@ -120,8 +148,9 @@ fn link_homebrew_m1(name: &str) -> PathBuf {
}
/// Find package. By default, it will try to find vcpkg first, then homebrew(currently only for Mac M1).
/// If building for linux and feature "linux-pkg-config" is enabled, will try to use pkg-config
/// unless check fails (e.g. NO_PKG_CONFIG_libyuv=1)
/// If building on a linux host and feature "linux-pkg-config" is enabled, will try to use pkg-config
/// unless check fails (e.g. NO_PKG_CONFIG_libyuv=1). Note the cfg! below sees the build host, not
/// the target, so cross builds (e.g. linux -> android) must not enable linux-pkg-config.
fn find_package(name: &str) -> Vec<PathBuf> {
let no_pkg_config_var_name = format!("NO_PKG_CONFIG_{name}");
println!("cargo:rerun-if-env-changed={no_pkg_config_var_name}");
@@ -170,9 +199,9 @@ fn gen_vcpkg_package(package: &str, ffi_header: &str, generated: &str, regex: &s
let out_dir = Path::new(&out_dir);
let ffi_header = src_dir.join("src").join("bindings").join(ffi_header);
println!("rerun-if-changed={}", ffi_header.display());
println!("cargo:rerun-if-changed={}", ffi_header.display());
for dir in &includes {
println!("rerun-if-changed={}", dir.display());
println!("cargo:rerun-if-changed={}", dir.display());
}
let ffi_rs = out_dir.join(generated);
@@ -248,6 +277,18 @@ fn main() {
gen_vcpkg_package("libvpx", "vpx_ffi.h", "vpx_ffi.rs", "^[vV].*");
gen_vcpkg_package("aom", "aom_ffi.h", "aom_ffi.rs", "^(aom|AOM|OBU|AV1).*");
gen_vcpkg_package("libyuv", "yuv_ffi.h", "yuv_ffi.rs", ".*");
// Skipped on 32-bit targets: svt-av1 does not support them, the vcpkg
// manifest does not install it there and disable_av1() never uses AV1 on
// them anyway. Must stay in sync with the cfg gates in
// mod.rs/codec.rs/video_service.rs.
if env::var("CARGO_CFG_TARGET_POINTER_WIDTH").as_deref() == Ok("64") {
gen_vcpkg_package(
"svt-av1",
"svt_av1_ffi.h",
"svt_av1_ffi.rs",
"^(svt_av1_|Svt|SVT_|Eb|EB_|PredStructure|PrivDataType).*",
);
}
// ffmpeg();
if target_os == "ios" {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
#include <svt-av1/EbSvtAv1.h>
#include <svt-av1/EbSvtAv1Enc.h>
#include <svt-av1/EbSvtAv1ErrorCodes.h>
#include <svt-av1/EbSvtAv1Formats.h>
#include <svt-av1/EbSvtAv1Metadata.h>

View File

@@ -9,6 +9,8 @@ use std::{
use crate::hwcodec::*;
#[cfg(feature = "mediacodec")]
use crate::mediacodec::{MediaCodecDecoder, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT};
#[cfg(target_pointer_width = "64")]
use crate::svt_av1::{SvtAv1Encoder, SvtAv1EncoderConfig};
#[cfg(feature = "vram")]
use crate::vram::*;
use crate::{
@@ -51,6 +53,8 @@ pub const ENCODE_NEED_SWITCH: &'static str = "ENCODE_NEED_SWITCH";
pub enum EncoderCfg {
VPX(VpxEncoderConfig),
AOM(AomEncoderConfig),
#[cfg(target_pointer_width = "64")]
SVTAV1(SvtAv1EncoderConfig),
#[cfg(feature = "hwcodec")]
HWRAM(HwRamEncoderConfig),
#[cfg(feature = "vram")]
@@ -71,6 +75,11 @@ pub trait EncoderApi {
fn set_quality(&mut self, ratio: f32) -> ResultType<()>;
/// Inform the encoder of the current pacing rate. Only rate controls that
/// budget per frame from a configured frame rate need to override the
/// default no-op.
fn set_fps(&mut self, _fps: u32) {}
fn bitrate(&self) -> u32;
fn support_changing_quality(&self) -> bool;
@@ -137,9 +146,34 @@ impl Encoder {
EncoderCfg::VPX(_) => Ok(Encoder {
codec: Box::new(VpxEncoder::new(config, i444)?),
}),
EncoderCfg::AOM(_) => Ok(Encoder {
codec: Box::new(AomEncoder::new(config, i444)?),
}),
EncoderCfg::AOM(_) => {
log::info!("AV1 encoder: aom");
Ok(Encoder {
codec: Box::new(AomEncoder::new(config, i444)?),
})
}
#[cfg(target_pointer_width = "64")]
EncoderCfg::SVTAV1(svt) => match SvtAv1Encoder::new(EncoderCfg::SVTAV1(svt), i444) {
Ok(codec) => {
log::info!("AV1 encoder: svt-av1");
Ok(Encoder {
codec: Box::new(codec),
})
}
Err(e) => {
// Same wire format, aom keeps the negotiated AV1 session alive.
log::error!("AV1 encoder: svt-av1 init failed ({e:?}), using aom");
let aom = EncoderCfg::AOM(AomEncoderConfig {
width: svt.width,
height: svt.height,
quality: svt.quality,
keyframe_interval: svt.keyframe_interval,
});
Ok(Encoder {
codec: Box::new(AomEncoder::new(aom, i444)?),
})
}
},
#[cfg(feature = "hwcodec")]
EncoderCfg::HWRAM(_) => match HwRamEncoder::new(config, i444) {
@@ -269,7 +303,10 @@ impl Encoder {
let preference = most_frequent.enum_value_or(PreferCodec::Auto);
// auto: h265 > h264 > av1/vp9/vp8
let av1_test = Config::get_option(hbb_common::config::keys::OPTION_AV1_TEST) != "N";
let av1_test = {
let v = Config::get_option(hbb_common::config::keys::OPTION_AV1_TEST);
v != "N" && v != "SVT-N"
};
let mut auto_codec = if av1_useable && av1_test {
CodecFormat::AV1
} else {
@@ -365,6 +402,8 @@ impl Encoder {
VpxVideoCodecId::VP9 => CodecFormat::VP9,
},
EncoderCfg::AOM(_) => CodecFormat::AV1,
#[cfg(target_pointer_width = "64")]
EncoderCfg::SVTAV1(_) => CodecFormat::AV1,
#[cfg(feature = "hwcodec")]
EncoderCfg::HWRAM(hw) => {
let name = hw.name.to_lowercase();
@@ -411,6 +450,10 @@ impl Encoder {
VpxVideoCodecId::VP9 => decodings.iter().all(|d| d.1.i444.vp9),
},
EncoderCfg::AOM(_) => decodings.iter().all(|d| d.1.i444.av1),
// Mirrors AOM so an i444 preference change still triggers the encoder
// rebuild, the rebuild then selects aom for the actual i444 encoding.
#[cfg(target_pointer_width = "64")]
EncoderCfg::SVTAV1(_) => decodings.iter().all(|d| d.1.i444.av1),
#[cfg(feature = "hwcodec")]
EncoderCfg::HWRAM(_) => false,
#[cfg(feature = "vram")]
@@ -1042,7 +1085,15 @@ pub fn test_av1() {
use hbb_common::rand::Rng;
use std::{sync::Once, time::Duration};
if disable_av1() || !Config::get_option(OPTION_AV1_TEST).is_empty() {
// The verdict is tied to the encoder generation that produced it, so
// switching the AV1 encoder (aom <-> svt-av1) re-runs the test.
#[cfg(target_pointer_width = "64")]
const AV1_TEST_RESULT: (&str, &str) = ("SVT-Y", "SVT-N");
#[cfg(not(target_pointer_width = "64"))]
const AV1_TEST_RESULT: (&str, &str) = ("Y", "N");
let cached = Config::get_option(OPTION_AV1_TEST);
if disable_av1() || cached == AV1_TEST_RESULT.0 || cached == AV1_TEST_RESULT.1 {
log::info!("skip test av1");
return;
}
@@ -1101,15 +1152,40 @@ pub fn test_av1() {
}
Ok(dst)
};
let Ok(mut av1) = AomEncoder::new(
EncoderCfg::AOM(AomEncoderConfig {
let aom = || {
AomEncoder::new(
EncoderCfg::AOM(AomEncoderConfig {
width,
height,
quality,
keyframe_interval,
}),
i444,
)
.map(|e| Box::new(e) as Box<dyn EncoderApi>)
};
// Test the encoder actually used for AV1, svt-av1 on 64-bit targets, with
// the same fallback to aom as Encoder::new so an svt init failure
// does not brand a working aom as unusable. Note that i444 (true
// color) sessions and svt-unsupported resolutions still run aom,
// whose speed is then not covered by this gate.
#[cfg(target_pointer_width = "64")]
let av1 = crate::svt_av1::SvtAv1Encoder::new(
EncoderCfg::SVTAV1(crate::svt_av1::SvtAv1EncoderConfig {
width,
height,
quality,
keyframe_interval,
qp_range: None,
preset: None,
}),
i444,
) else {
)
.map(|e| Box::new(e) as Box<dyn EncoderApi>)
.or_else(|_| aom());
#[cfg(not(target_pointer_width = "64"))]
let av1 = aom();
let Ok(mut av1) = av1 else {
return false;
};
let mut key_frame_time = Duration::ZERO;
@@ -1122,7 +1198,7 @@ pub fn test_av1() {
};
let start = Instant::now();
if av1
.encode(pts.elapsed().as_millis() as _, &yuv, super::STRIDE_ALIGN)
.encode_to_message(EncodeInput::YUV(&yuv), pts.elapsed().as_millis() as _)
.is_err()
{
log::debug!("av1 encode failed");
@@ -1150,7 +1226,12 @@ pub fn test_av1() {
let v = f();
Config::set_option(
OPTION_AV1_TEST.to_string(),
if v { "Y" } else { "N" }.to_string(),
if v {
AV1_TEST_RESULT.0
} else {
AV1_TEST_RESULT.1
}
.to_string(),
);
});
});

View File

@@ -52,6 +52,8 @@ pub mod aom;
#[cfg(not(any(target_os = "ios")))]
pub mod camera;
pub mod record;
#[cfg(target_pointer_width = "64")]
pub mod svt_av1;
mod vpx;
#[repr(usize)]

View File

@@ -0,0 +1,536 @@
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(improper_ctypes)]
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/svt_av1_ffi.rs"));
use crate::codec::{base_bitrate, codec_thread_num, EncoderApi, EncoderCfg};
use crate::{EncodeInput, EncodeYuvFormat, Pixfmt, STRIDE_ALIGN};
use hbb_common::{
anyhow::Context,
bail,
bytes::Bytes,
log,
message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
ResultType,
};
use std::{mem::MaybeUninit, os::raw::c_void, ptr, slice};
// SVT-AV1 rejects on-the-fly target bitrates above 100_000 kbps.
const MAX_TARGET_BITRATE_KBPS: u32 = 100_000;
// CBR budgets bits per frame from the configured frame rate, VideoQoS pushes
// its authoritative pacing rate in via set_fps.
const DEFAULT_FPS: u32 = 30;
#[derive(Clone, Copy, Debug)]
pub struct SvtAv1EncoderConfig {
pub width: u32,
pub height: u32,
pub quality: f32,
pub keyframe_interval: Option<usize>,
// SVT cannot update min/max QP on the fly. Product callers leave this
// unset so dynamic quality changes retain the full project QP envelope;
// controlled comparisons can pin the same range used by another encoder.
pub qp_range: Option<(u32, u32)>,
// RTC supports M7 through M13. Product callers use the screen-content
// default; benchmarks can override it to measure the speed/quality curve.
pub preset: Option<i8>,
}
pub struct SvtAv1Encoder {
handle: *mut EbComponentType,
width: usize,
height: usize,
yuvfmt: EncodeYuvFormat,
// current target bitrate in kbps, same unit as aom's rc_target_bitrate
bitrate: u32,
// bitrate change (kbps) to apply with the next picture via RATE_CHANGE_EVENT
pending_bitrate: Option<u32>,
// frame rate currently configured in the encoder
fps: u32,
// frame rate change to apply with the next picture via FRAME_RATE_CHANGE_EVENT
pending_fps: Option<u32>,
}
// The handle is only used behind &mut self, SVT-AV1 synchronizes internally.
unsafe impl Send for SvtAv1Encoder {}
impl EncoderApi for SvtAv1Encoder {
fn new(cfg: EncoderCfg, i444: bool) -> ResultType<Self>
where
Self: Sized,
{
match cfg {
EncoderCfg::SVTAV1(config) => {
if i444 {
// SVT-AV1 only supports 4:2:0 input, callers must fall back to aom for i444.
bail!("svt-av1 encoder does not support I444");
}
if !Self::support(config.width, config.height) {
bail!(
"svt-av1 encoder does not support resolution {}x{}",
config.width,
config.height
);
}
if let Some((min_qp, max_qp)) = config.qp_range {
if min_qp > max_qp || max_qp > 63 {
bail!("invalid svt-av1 QP range {min_qp}..{max_qp}");
}
}
if let Some(preset) = config.preset {
if !(7..=13).contains(&preset) {
bail!("invalid svt-av1 RTC preset M{preset}; expected M7..M13");
}
}
let mut handle: *mut EbComponentType = ptr::null_mut();
let mut c: MaybeUninit<EbSvtAv1EncConfiguration> = MaybeUninit::zeroed();
let res = unsafe { svt_av1_enc_init_handle(&mut handle, c.as_mut_ptr()) };
if res != EbErrorType::EB_ErrorNone || handle.is_null() {
bail!("svt_av1_enc_init_handle failed: {res:?}");
}
// c is loaded with the library defaults now, only override what we need.
let mut c = unsafe { c.assume_init() };
let bitrate = Self::bitrate(config.width, config.height, config.quality)
.min(MAX_TARGET_BITRATE_KBPS);
Self::apply_config(&mut c, &config, bitrate);
let mut res = unsafe { svt_av1_enc_set_parameter(handle, &mut c) };
if res == EbErrorType::EB_ErrorNone {
res = unsafe { svt_av1_enc_init(handle) };
}
if res != EbErrorType::EB_ErrorNone {
unsafe {
svt_av1_enc_deinit_handle(handle);
}
bail!("failed to init svt-av1 encoder: {res:?}");
}
Ok(Self {
handle,
width: config.width as _,
height: config.height as _,
yuvfmt: Self::get_yuvfmt(config.width, config.height),
bitrate,
pending_bitrate: None,
fps: DEFAULT_FPS,
pending_fps: None,
})
}
_ => bail!("encoder type mismatch"),
}
}
fn encode_to_message(&mut self, input: EncodeInput, ms: i64) -> ResultType<VideoFrame> {
let frames = self
.encode(ms, input.yuv()?)
.with_context(|| "Failed to encode")?;
if frames.len() > 0 {
Ok(Self::create_video_frame(frames))
} else {
bail!("no valid frame");
}
}
fn yuvfmt(&self) -> EncodeYuvFormat {
self.yuvfmt.clone()
}
#[cfg(feature = "vram")]
fn input_texture(&self) -> bool {
false
}
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
let bitrate =
Self::bitrate(self.width as _, self.height as _, ratio).min(MAX_TARGET_BITRATE_KBPS);
if bitrate > 0 && bitrate != self.bitrate {
self.bitrate = bitrate;
self.pending_bitrate = Some(bitrate);
}
Ok(())
}
fn set_fps(&mut self, fps: u32) {
if fps == 0 {
return;
}
// also cancels a queued change that a later call made moot again
if fps == self.fps {
self.pending_fps = None;
} else {
self.pending_fps = Some(fps);
}
}
fn bitrate(&self) -> u32 {
self.bitrate
}
fn support_changing_quality(&self) -> bool {
true
}
fn latency_free(&self) -> bool {
true
}
fn is_hardware(&self) -> bool {
false
}
fn disable(&self) {}
}
impl SvtAv1Encoder {
pub fn support(width: u32, height: u32) -> bool {
width >= 64
&& height >= 64
&& width <= 16384
&& height <= 8704
&& width % 2 == 0
&& height % 2 == 0
}
fn apply_config(c: &mut EbSvtAv1EncConfiguration, cfg: &SvtAv1EncoderConfig, bitrate: u32) {
c.enc_mode = cfg
.preset
.unwrap_or_else(|| Self::preset(cfg.width, cfg.height));
c.source_width = cfg.width;
c.source_height = cfg.height;
// CBR budgets bits per frame from this rate, set_fps adjusts it on the
// fly with FRAME_RATE_CHANGE_EVENT.
c.frame_rate_numerator = DEFAULT_FPS;
c.frame_rate_denominator = 1;
c.encoder_bit_depth = 8;
c.encoder_color_format = EbColorFormat::EB_YUV420;
c.profile = EbAv1SeqProfile::MAIN_PROFILE;
// Low delay + rtc + CBR: one packet out per picture in, svt_av1_enc_get_packet
// blocks until the packet for the sent picture is ready, and rate/keyframe
// changes on the fly are allowed.
c.pred_structure = PredStructure::LOW_DELAY;
// A four-picture temporal hierarchy improves reference efficiency without
// introducing B-frame reordering. Two is also the highest hierarchy SVT
// validates for low-delay CBR, and still preserves one packet per input.
c.hierarchical_levels = 2;
c.rtc = true;
c.rate_control_mode = SvtAv1RcMode::SVT_AV1_RC_MODE_CBR as _;
c.target_bit_rate = bitrate.min(MAX_TARGET_BITRATE_KBPS) * 1000;
let initial_qp_range = Self::quality_qp_range(cfg.quality);
let (min_qp, max_qp) = cfg.qp_range.unwrap_or((5, 45));
c.min_qp_allowed = min_qp;
c.max_qp_allowed = max_qp;
c.qp = ((initial_qp_range.0 + initial_qp_range.1) / 2).clamp(min_qp, max_qp);
c.look_ahead_distance = 0;
// SVT 4.2's RTC CBR path only re-runs mode decision once when an inter
// frame would overflow the virtual buffer. This keeps network bursts
// bounded without enabling unrestricted multi-pass re-encoding.
c.recode_loop = 1; // ALLOW_RECODE_KFMAXBW / one RTC VBV recode
c.scene_change_detection = 0;
// Temporal filtering is primarily useful for camera/video content. It
// costs CPU and can soften text, while low-delay screen sharing cannot
// benefit from future-frame filtering.
c.enable_tf = 0;
c.enable_tf_key = false;
c.enable_overlays = false;
// Use SVT's anti-alias-aware adaptive screen detection: UI/text can use
// Palette and IntraBC while camera/video regions stay on the natural-
// content path. RTC only supports these tools at M8 or slower.
c.screen_content_mode = 3;
c.enable_intrabc = true;
c.tune = 1; // PSNR, low delay does not support tune 0
c.level_of_parallelism = Self::parallelism();
c.intra_refresh_type = SvtAv1IntraRefreshType::SVT_AV1_KF_REFRESH; // closed GOP
c.intra_period_length = match cfg.keyframe_interval {
Some(keyframe_interval) => keyframe_interval.saturating_sub(1) as _,
None => -1, // no periodic intra refresh, keyframes only on demand
};
// pic_type is left to the encoder and keyframes come from encoder
// restarts, the force_key_frames flag must stay off for low delay CBR
// (the library resets it with a warning).
c.force_key_frames = false;
}
fn preset(_width: u32, _height: u32) -> i8 {
// SVT-AV1 forces screen_content_mode to 0 for RTC presets M9 and faster,
// so use M8 to keep the screen-content path enabled.
// TODO: After benchmarking, select a quality-dependent preset in M0..=M8.
8
}
fn parallelism() -> u32 {
// level_of_parallelism is a level from 1 to 6, not a thread count.
match codec_thread_num(64) {
n if n >= 32 => 6,
n if n >= 16 => 5,
n if n >= 8 => 4,
n if n >= 4 => 3,
n if n >= 2 => 2,
_ => 1,
}
}
fn encode(&mut self, ms: i64, data: &[u8]) -> ResultType<Vec<EncodedVideoFrame>> {
let fmt = &self.yuvfmt;
let chroma_height = (fmt.h + 1) / 2;
let len = fmt.v + fmt.stride[2] * chroma_height;
if data.len() < len {
bail!("len not enough: {} < {}", data.len(), len);
}
let mut io = EbSvtIOFormat {
luma: data.as_ptr() as *mut u8,
cb: data[fmt.u..].as_ptr() as *mut u8,
cr: data[fmt.v..].as_ptr() as *mut u8,
y_stride: fmt.stride[0] as _,
cr_stride: fmt.stride[2] as _,
cb_stride: fmt.stride[1] as _,
};
let mut hdr: EbBufferHeaderType = unsafe { std::mem::zeroed() };
hdr.size = std::mem::size_of::<EbBufferHeaderType>() as _;
hdr.p_buffer = &mut io as *mut EbSvtIOFormat as *mut u8;
hdr.n_filled_len = len as _;
hdr.pts = ms;
hdr.pic_type = EbAv1PictureType::EB_AV1_INVALID_PICTURE; // encoder decides
// send_picture copies the private data list, stack lifetime is fine.
// The pending changes are only cleared after a successful send, so a
// failed send retries them with the next picture.
let pending_bitrate = self.pending_bitrate;
let pending_fps = self.pending_fps;
let mut rate_info = SvtAv1RateInfo {
seq_qp: 0,
target_bit_rate: pending_bitrate.unwrap_or(0).min(MAX_TARGET_BITRATE_KBPS) * 1000,
};
let mut fps_info = SvtAv1FrameRateInfo {
frame_rate_numerator: pending_fps.unwrap_or(0),
frame_rate_denominator: 1,
};
let mut rate_node = EbPrivDataNode {
node_type: PrivDataType::RATE_CHANGE_EVENT,
data: &mut rate_info as *mut SvtAv1RateInfo as *mut c_void,
size: std::mem::size_of::<SvtAv1RateInfo>() as _,
next: ptr::null_mut(),
};
let mut fps_node = EbPrivDataNode {
node_type: PrivDataType::FRAME_RATE_CHANGE_EVENT,
data: &mut fps_info as *mut SvtAv1FrameRateInfo as *mut c_void,
size: std::mem::size_of::<SvtAv1FrameRateInfo>() as _,
next: ptr::null_mut(),
};
let mut list: *mut EbPrivDataNode = ptr::null_mut();
if pending_bitrate.is_some() {
list = &mut rate_node;
}
if pending_fps.is_some() {
fps_node.next = list;
list = &mut fps_node;
}
hdr.p_app_private = list as *mut c_void;
// The input YUV planes are copied inside send_picture.
let res = unsafe { svt_av1_enc_send_picture(self.handle, &mut hdr) };
if res != EbErrorType::EB_ErrorNone {
bail!("svt_av1_enc_send_picture failed: {res:?}");
}
self.pending_bitrate = None;
self.pending_fps = None;
if let Some(fps) = pending_fps {
self.fps = fps;
}
// In low delay mode get_packet blocks until the packet for the picture just
// sent is ready, and each picture produces exactly one packet. Do not call
// it again without sending another picture, it would block forever.
let mut frames = Vec::new();
let mut pkt: *mut EbBufferHeaderType = ptr::null_mut();
let res = unsafe { svt_av1_enc_get_packet(self.handle, &mut pkt, 0) };
match res {
EbErrorType::EB_ErrorNone if !pkt.is_null() => unsafe {
let h = &*pkt;
frames.push(EncodedVideoFrame {
data: Bytes::from(
slice::from_raw_parts(h.p_buffer, h.n_filled_len as usize).to_vec(),
),
key: h.pic_type == EbAv1PictureType::EB_AV1_KEY_PICTURE,
pts: h.pts,
..Default::default()
});
svt_av1_enc_release_out_buffer(&mut pkt);
},
EbErrorType::EB_NoErrorEmptyQueue => {}
_ => {
if !pkt.is_null() {
unsafe {
svt_av1_enc_release_out_buffer(&mut pkt);
}
}
bail!("svt_av1_enc_get_packet failed: {res:?}");
}
}
Ok(frames)
}
#[inline]
fn create_video_frame(frames: Vec<EncodedVideoFrame>) -> VideoFrame {
let mut vf = VideoFrame::new();
let av1s = EncodedVideoFrames {
frames: frames.into(),
..Default::default()
};
vf.set_av1s(av1s);
vf
}
fn bitrate(width: u32, height: u32, ratio: f32) -> u32 {
let bitrate = base_bitrate(width, height) as f32;
(bitrate * ratio) as u32
}
// Same mapping as AomEncoder::calc_q_values.
#[inline]
pub fn quality_qp_range(ratio: f32) -> (u32, u32) {
let b = (ratio * 100.0) as u32;
let b = std::cmp::min(b, 200);
let q_min1 = 24;
let q_min2 = 5;
let q_max1 = 45;
let q_max2 = 25;
let t = b as f32 / 200.0;
let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32;
let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32;
q_min = q_min.clamp(q_min2, q_min1);
q_max = q_max.clamp(q_max2, q_max1);
(q_min, q_max)
}
fn get_yuvfmt(width: u32, height: u32) -> EncodeYuvFormat {
let w = width as usize;
let h = height as usize;
let align = |x: usize| (x + STRIDE_ALIGN - 1) & !(STRIDE_ALIGN - 1);
let stride_y = align(w);
let stride_uv = align((w + 1) / 2);
let u = stride_y * h;
let v = u + stride_uv * ((h + 1) / 2);
EncodeYuvFormat {
pixfmt: Pixfmt::I420,
w,
h,
stride: vec![stride_y, stride_uv, stride_uv],
u,
v,
}
}
}
impl Drop for SvtAv1Encoder {
fn drop(&mut self) {
unsafe {
if self.handle.is_null() {
return;
}
// svt_av1_enc_deinit drains the pipeline itself, but logs an error
// when EOS was not sent first.
let mut hdr: EbBufferHeaderType = std::mem::zeroed();
hdr.size = std::mem::size_of::<EbBufferHeaderType>() as _;
hdr.pic_type = EbAv1PictureType::EB_AV1_INVALID_PICTURE;
hdr.flags = EB_BUFFERFLAG_EOS;
let _ = svt_av1_enc_send_picture(self.handle, &mut hdr);
let res = svt_av1_enc_deinit(self.handle);
if res != EbErrorType::EB_ErrorNone {
log::error!("svt_av1_enc_deinit failed: {res:?}");
}
let res = svt_av1_enc_deinit_handle(self.handle);
if res != EbErrorType::EB_ErrorNone {
log::error!("svt_av1_enc_deinit_handle failed: {res:?}");
}
self.handle = ptr::null_mut();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::GoogleImage;
fn fake_i420(fmt: &EncodeYuvFormat, index: usize) -> Vec<u8> {
let chroma_height = (fmt.h + 1) / 2;
let len = fmt.v + fmt.stride[2] * chroma_height;
let mut yuv = vec![128u8; len];
// moving gradient so every frame differs
for y in 0..fmt.h {
let row = &mut yuv[y * fmt.stride[0]..y * fmt.stride[0] + fmt.w];
for (x, px) in row.iter_mut().enumerate() {
*px = ((x + y + index * 17) % 256) as u8;
}
}
yuv
}
#[test]
fn test_encode_and_decode_with_aom() {
let (width, height) = (640u32, 480u32);
let mut enc = SvtAv1Encoder::new(
crate::codec::EncoderCfg::SVTAV1(SvtAv1EncoderConfig {
width,
height,
quality: 1.0,
keyframe_interval: None,
qp_range: None,
preset: None,
}),
false,
)
.unwrap();
let fmt = enc.yuvfmt();
let mut dec = crate::aom::AomDecoder::new().unwrap();
for i in 0..20usize {
if i == 5 {
// exercises the FRAME_RATE_CHANGE_EVENT path on the next frame
enc.set_fps(60);
}
if i == 10 {
// exercises the RATE_CHANGE_EVENT path on the next frame
enc.set_quality(0.5).unwrap();
}
let yuv = fake_i420(&fmt, i);
let frames = enc.encode(i as i64 * 33, &yuv).unwrap();
// one packet out per picture in, this is the low delay contract
assert_eq!(frames.len(), 1, "no packet for frame {i}");
assert_eq!(frames[0].key, i == 0, "unexpected key flag for frame {i}");
assert!(!frames[0].data.is_empty());
let mut decoded = 0;
for f in &frames {
for img in dec.decode(&f.data).unwrap() {
assert_eq!(img.width(), width as usize);
assert_eq!(img.height(), height as usize);
decoded += 1;
}
}
for img in dec.flush().unwrap() {
assert_eq!(img.width(), width as usize);
decoded += 1;
}
assert!(decoded >= 1, "aom failed to decode svt-av1 frame {}", i);
}
}
#[test]
fn test_unsupported_resolution() {
assert!(SvtAv1Encoder::new(
crate::codec::EncoderCfg::SVTAV1(SvtAv1EncoderConfig {
width: 62,
height: 62,
quality: 1.0,
keyframe_interval: None,
qp_range: None,
preset: None,
}),
false,
)
.is_err());
}
}

View File

@@ -0,0 +1,10 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -194,5 +194,5 @@
#Clang support, required to build static with LTO
-if(CMAKE_C_COMPILER_ID MATCHES "Clang" AND UNIX AND NOT APPLE)
+if(FALSE)
find_program(LLVM_LD_EXE llvm-ld)
find_program(LLVM_AR_EXE llvm-ar)
find_program(LLVM_RANLIB_EXE llvm-ranlib)

View File

@@ -0,0 +1,14 @@
diff --git a/Source/Lib/ASM_AVX2/synonyms_avx2.h b/Source/Lib/ASM_AVX2/synonyms_avx2.h
--- a/Source/Lib/ASM_AVX2/synonyms_avx2.h
+++ b/Source/Lib/ASM_AVX2/synonyms_avx2.h
@@ -86,3 +86,9 @@
-static INLINE __m256i yy_unpacklo_epi128(const __m256i in0, const __m256i in1) {
+#if defined(_MSC_VER) && _MSC_VER >= 1950
+#define MAYBE_INLINE NOINLINE
+#else
+#define MAYBE_INLINE INLINE
+#endif
+
+static MAYBE_INLINE __m256i yy_unpacklo_epi128(const __m256i in0, const __m256i in1) {
return _mm256_inserti128_si256(in0, _mm256_castsi256_si128(in1), 1);
}

View File

@@ -0,0 +1,45 @@
vcpkg_from_gitlab(
GITLAB_URL https://gitlab.com
OUT_SOURCE_PATH SOURCE_PATH
REPO AOMediaCodec/SVT-AV1
REF v${VERSION}
SHA512 37df96559af179d28acae10cdff98c94ee2c4ee086b2446ab362b57be9adf566d6f2adc28faa30648798d9bc7d18eaeb2d0665e7292982652496b605342e1c4b
PATCHES
# upstream forces llvm-ld/llvm-ar/llvm-ranlib for clang static builds on
# non-Apple unix, which breaks toolchains without the llvm binutils
no-force-llvm.diff
# MSVC >= 1950 miscompiles the inlined yy_unpacklo_epi128
no-inline-yy_unpacklo_epi128.diff
)
if(VCPKG_TARGET_ARCHITECTURE MATCHES "^(x86|x64)$")
# NASM is required to build the x86/x64 assembly
vcpkg_find_acquire_program(NASM)
set(SIMD_OPTIONS -DCOMPILE_C_ONLY=OFF "-DCMAKE_ASM_NASM_COMPILER=${NASM}")
elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(arm64|arm64ec)$" AND NOT VCPKG_TARGET_IS_WINDOWS)
set(SIMD_OPTIONS -DCOMPILE_C_ONLY=OFF)
else()
set(SIMD_OPTIONS -DCOMPILE_C_ONLY=ON)
endif()
vcpkg_cmake_configure(
SOURCE_PATH "${SOURCE_PATH}"
OPTIONS
${SIMD_OPTIONS}
-DBUILD_APPS=OFF
-DBUILD_TESTING=OFF
-DREPRODUCIBLE_BUILDS=ON
# SVT-AV1 defaults LTO to ON for gcc>=9/clang>=12, which puts LTO
# bitcode into the static archive and breaks linking with a different
# compiler version
-DSVT_AV1_LTO=OFF
)
vcpkg_cmake_install()
vcpkg_cmake_config_fixup(PACKAGE_NAME SVT-AV1 CONFIG_PATH lib/cmake/SVT-AV1)
vcpkg_copy_pdbs()
vcpkg_fixup_pkgconfig()
file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.md" "${SOURCE_PATH}/PATENTS.md")

View File

@@ -0,0 +1,19 @@
{
"name": "svt-av1",
"version-semver": "4.2.0",
"port-version": 0,
"description": "Scalable Video Technology AV1 software video encoder library",
"homepage": "https://gitlab.com/AOMediaCodec/SVT-AV1",
"license": "BSD-3-Clause-Clear",
"supports": "!x86 & !arm32 & !uwp",
"dependencies": [
{
"name": "vcpkg-cmake",
"host": true
},
{
"name": "vcpkg-cmake-config",
"host": true
}
]
}

View File

@@ -1024,7 +1024,7 @@ pub fn get_full_name() -> String {
}
pub fn is_setup(name: &str) -> bool {
name.to_lowercase().ends_with("install.exe")
!config::is_disable_installation() && name.to_lowercase().ends_with("install.exe")
}
pub fn get_custom_rendezvous_server(custom: String) -> String {
@@ -2623,6 +2623,20 @@ pub fn is_direct_ip_access(peer: &str) -> bool {
hbb_common::is_ip_str(peer) || hbb_common::is_domain_port_str(peer)
}
// Align the maximum length of the peer id to the maximum length of the peer id in the server.
const MAX_UNTRUSTED_PEER_ID_LEN: usize = 253;
const UNTRUSTED_PEER_ID_FORBIDDEN_CHARS: &[char] = &['"', '<', '>', '/', '\\', '|', '?', '*'];
// Shared validation for peer/connect ids that cross untrusted boundaries before
// they are stored or written into command/script contexts.
pub fn is_valid_untrusted_peer_id(id: &str) -> bool {
!id.is_empty()
&& id.len() <= MAX_UNTRUSTED_PEER_ID_LEN
&& !id.chars().any(|ch| {
ch.is_control() || ch.is_whitespace() || UNTRUSTED_PEER_ID_FORBIDDEN_CHARS.contains(&ch)
})
}
#[cfg(test)]
mod tests {
use super::*;
@@ -2653,6 +2667,29 @@ mod tests {
)
}
#[test]
fn untrusted_peer_id_validation() {
let cases = [
("123456789", true),
("m\u{00FC}nchen-pc", true),
("192.168.1.10:21118", true),
("9123456234@public", true),
(
r#"1" & oWS.Run("cmd.exe /k whoami /priv",1,False) & ""#,
false,
),
("", false),
("peer id", false),
("peer\nid", false),
("peer/id", false),
("peer?id", false),
];
for (id, expected) in cases {
assert_eq!(is_valid_untrusted_peer_id(id), expected, "{id:?}");
}
}
// ThrottledInterval tick at the same time as tokio interval, if no sleeps
#[allow(non_snake_case)]
#[tokio::test]

View File

@@ -127,6 +127,13 @@ pub fn core_main() -> Option<Vec<String>> {
if args.contains(&"--noinstall".to_string()) {
args.clear();
}
// The portable wrapper injects `--install` when its name ends with `install.exe`,
// including `no-install.exe`. Drop the argument instead of exiting so disabled
// clients can continue running as portable applications.
if config::is_disable_installation() {
args.retain(|arg| arg != "--install");
flutter_args.retain(|arg| arg != "--install");
}
if args.len() > 0 {
if args[0] == "--version" {
println!("{}", crate::VERSION);
@@ -660,7 +667,8 @@ pub fn core_main() -> Option<Vec<String>> {
None
}
};
let new_id = get_value("--id");
// An empty --id (e.g. an unset var) would deploy a blank id; the Android flow guards this too (#15146).
let new_id = get_value("--id").filter(|s| !s.is_empty());
match crate::ui_interface::deploy_device(token, new_id) {
crate::ui_interface::DeployResult::Ok => {
println!("Device deployed.");

View File

@@ -136,6 +136,12 @@ pub extern "C" fn rustdesk_core_main_args(args_len: *mut c_int) -> *mut *mut c_c
return std::ptr::null_mut() as _;
}
#[cfg(windows)]
#[no_mangle]
pub extern "C" fn rustdesk_is_disable_installation() -> c_int {
hbb_common::config::is_disable_installation() as c_int
}
// https://gist.github.com/iskakaushik/1c5b8aa75c77479c33c4320913eebef6
#[cfg(windows)]
fn rust_args_to_c_args(args: Vec<String>, outlen: *mut c_int) -> *mut *mut c_char {

View File

@@ -881,8 +881,14 @@ async fn handle(data: Data, stream: &mut Connection) {
Some(value) => {
let mut updated = true;
if name == "id" {
Config::set_key_confirmed(false);
Config::set_id(&value);
// An empty id would wipe the local id and unconfirm the key (cf. #15626).
if value.is_empty() {
log::warn!("Ignoring empty id write over IPC");
updated = false;
} else {
Config::set_key_confirmed(false);
Config::set_id(&value);
}
} else if name == "temporary-password" {
password::update_temporary_password();
} else if name == "permanent-password" {
@@ -1689,19 +1695,24 @@ pub fn clear_trusted_devices() {
}
pub fn get_id() -> String {
// An empty id may come from a process that took over the main IPC with a
// config scope that has no id yet (e.g. a user GUI that became the server
// while the installed service was restarting). Treat it as no answer,
// otherwise the empty id is adopted below and wipes the local one.
if let Ok(Some(v)) = get_config("id") {
// update salt also, so that next time reinstallation not causing first-time auto-login failure
if let Ok(Some(v2)) = get_config("salt") {
Config::set_salt(&v2);
if !v.is_empty() {
// update salt also, so that next time reinstallation not causing first-time auto-login failure
if let Ok(Some(v2)) = get_config("salt") {
Config::set_salt(&v2);
}
if v != Config::get_id() {
Config::set_key_confirmed(false);
Config::set_id(&v);
}
return v;
}
if v != Config::get_id() {
Config::set_key_confirmed(false);
Config::set_id(&v);
}
v
} else {
Config::get_id()
}
Config::get_id()
}
pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec<String>) {

View File

@@ -241,6 +241,14 @@ fn wait_response(
Some(rendezvous_message::Union::PeerDiscovery(p)) => {
last_recv_time = Instant::now();
if p.cmd == "pong" {
if !crate::common::is_valid_untrusted_peer_id(&p.id) {
log::warn!(
"Ignoring LAN discovery response from {} with invalid peer id",
addr
);
continue;
}
let local_mac = if try_get_ip_by_peer {
if let Some(self_addr) = get_ipaddr_by_peer(&addr) {
get_mac(&self_addr)

View File

@@ -332,7 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "中继连接"),
("Secure Connection", "安全连接"),
("Insecure Connection", "非安全连接"),
("Continue", ""),
("Continue", "继续"),
("Scale original", "原始尺寸"),
("Scale adaptive", "适应窗口"),
("General", "常规"),

View File

@@ -332,7 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Połączenie przez bramkę"),
("Secure Connection", "Połączenie szyfrowane"),
("Insecure Connection", "Połączenie nieszyfrowane"),
("Continue", ""),
("Continue", "Kontynuuj"),
("Scale original", "Skalowanie oryginalne"),
("Scale adaptive", "Dopasuj do wyświetlacza"),
("General", "Ogólne"),

View File

@@ -112,7 +112,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Waiting", "Aguardando"),
("Finished", "Concluído"),
("Speed", "Velocidade"),
("Custom Image Quality", "Qualidade Visual Personalizada"),
("Custom Image Quality", "Qualidade de imagem personalizada"),
("Privacy mode", "Modo privado"),
("Block user input", "Bloquear entrada do usuário"),
("Unblock user input", "Desbloquear entrada do usuário"),
@@ -122,15 +122,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stretch", "Aumentar"),
("Scrollbar", "Barra de rolagem"),
("ScrollAuto", "Rolagem automática"),
("Good image quality", "Boa qualidade de imagem"),
("Good image quality", "Melhor qualidade"),
("Balanced", "Balanceada"),
("Optimize reaction time", "Otimizar tempo de resposta"),
("Custom", "Personalizado"),
("Custom", "Personalizada"),
("Show remote cursor", "Mostrar cursor remoto"),
("Show quality monitor", "Exibir monitor de qualidade"),
("Disable clipboard", "Desabilitar área de transferência"),
("Lock after session end", "Bloquear após o fim da sessão"),
("Insert Ctrl + Alt + Del", "Enviar Ctrl + Alt + Del"),
("Insert Ctrl + Alt + Del", "Enviar Ctrl+Alt+Del"),
("Insert Lock", "Bloquear computador"),
("Refresh", "Atualizar"),
("ID does not exist", "ID não existe"),
@@ -332,7 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relay Connection", "Conexão via Relay"),
("Secure Connection", "Conexão Segura"),
("Insecure Connection", "Conexão Insegura"),
("Continue", ""),
("Continue", "Continuar"),
("Scale original", "Escala original"),
("Scale adaptive", "Escala adaptada"),
("General", "Geral"),

View File

@@ -646,6 +646,16 @@ fn try_start_server_(desktop: Option<&Desktop>) -> ResultType<Option<Child>> {
if !desktop.dbus.is_empty() {
envs.push(("DBUS_SESSION_BUS_ADDRESS", desktop.dbus.clone()));
}
if let Ok(forced_display_server) =
std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER")
{
if !forced_display_server.is_empty() {
envs.push((
"RUSTDESK_FORCED_DISPLAY_SERVER",
forced_display_server,
));
}
}
envs.push((
"TERM",
get_cur_term(&desktop.uid).unwrap_or_else(|| suggest_best_term()),

View File

@@ -118,16 +118,18 @@ extern "C" bool MacCheckAdminAuthorization() {
// https://gist.github.com/briankc/025415e25900750f402235dbf1b74e42
extern "C" float BackingScaleFactor(uint32_t display) {
NSArray<NSScreen *> *screens = [NSScreen screens];
for (NSScreen *screen in screens) {
NSDictionary *deviceDescription = [screen deviceDescription];
NSNumber *screenNumber = [deviceDescription objectForKey:@"NSScreenNumber"];
CGDirectDisplayID screenDisplayID = [screenNumber unsignedIntValue];
if (screenDisplayID == display) {
return [screen backingScaleFactor];
@autoreleasepool {
NSArray<NSScreen *> *screens = [NSScreen screens];
for (NSScreen *screen in screens) {
NSDictionary *deviceDescription = [screen deviceDescription];
NSNumber *screenNumber = [deviceDescription objectForKey:@"NSScreenNumber"];
CGDirectDisplayID screenDisplayID = [screenNumber unsignedIntValue];
if (screenDisplayID == display) {
return [screen backingScaleFactor];
}
}
return 1;
}
return 1;
}
// https://github.com/jhford/screenresolution/blob/master/cg_utils.c

View File

@@ -2278,6 +2278,10 @@ fn get_shortcut_icon_location(install_dir: &str, exe: &str) -> String {
}
pub fn create_shortcut(id: &str) -> ResultType<()> {
if !crate::common::is_valid_untrusted_peer_id(id) {
bail!("Invalid peer id for shortcut");
}
let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
// https://github.com/rustdesk/rustdesk/issues/13735
// Replace ':' with '_' for filename since ':' is not allowed in Windows filenames

View File

@@ -2807,7 +2807,6 @@ impl Connection {
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(lr) = _s.lr.clone().take() {
self.handle_login_request_without_validation(&lr).await;
SWITCH_SIDES_UUID
.lock()
.unwrap()
@@ -2816,6 +2815,15 @@ impl Connection {
if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) {
if let Some((_instant, uuid_old)) = uuid_old {
if uuid == uuid_old {
if lr.union.is_some() {
log::warn!(
"Rejected switch sides response for non-remote-desktop session; closing connection"
);
self.send_login_error("Connection not allowed").await;
return false;
}
self.reset_session_scope_for_login();
self.handle_login_request_without_validation(&lr).await;
self.from_switch = true;
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::SwitchSides);
if !self.send_logon_response_and_keep_alive().await {
@@ -5656,6 +5664,7 @@ impl Connection {
Some(misc::Union::ChangeDisplayResolution(_)) => "misc.change_display_resolution",
Some(misc::Union::MessageQuery(_)) => "misc.message_query",
Some(misc::Union::FollowCurrentDisplay(_)) => "misc.follow_current_display",
Some(misc::Union::SwitchSidesRequest(_)) => "misc.switch_sides_request",
Some(_) => "misc.other",
None => "misc.empty",
}
@@ -6773,6 +6782,10 @@ mod test {
misc_msg(|m| m.set_capture_displays(CaptureDisplays::new())),
Some("misc.capture_displays"),
),
(
misc_msg(|m| m.set_switch_sides_request(SwitchSidesRequest::new())),
Some("misc.switch_sides_request"),
),
(msg(|m| m.set_clipboard(Clipboard::new())), None),
(
msg(|m| m.set_multi_clipboards(MultiClipboards::new())),
@@ -6817,6 +6830,10 @@ mod test {
misc_msg(|m| m.set_toggle_privacy_mode(TogglePrivacyMode::new())),
Some("misc.toggle_privacy_mode"),
),
(
misc_msg(|m| m.set_switch_sides_request(SwitchSidesRequest::new())),
Some("misc.switch_sides_request"),
),
(misc_msg(|m| m.set_chat_message(ChatMessage::new())), None),
(msg(|m| m.set_clipboard(Clipboard::new())), None),
(
@@ -6902,6 +6919,10 @@ mod test {
msg(|m| m.set_terminal_action(TerminalAction::new())),
Some("terminal_action"),
),
(
misc_msg(|m| m.set_switch_sides_request(SwitchSidesRequest::new())),
Some("misc.switch_sides_request"),
),
],
),
(
@@ -6912,6 +6933,10 @@ mod test {
None,
),
(msg(|m| m.set_terminal_action(TerminalAction::new())), None),
(
misc_msg(|m| m.set_switch_sides_request(SwitchSidesRequest::new())),
None,
),
],
),
(
@@ -6931,6 +6956,10 @@ mod test {
msg(|m| m.set_screenshot_request(ScreenshotRequest::new())),
Some("screenshot_request"),
),
(
misc_msg(|m| m.set_switch_sides_request(SwitchSidesRequest::new())),
Some("misc.switch_sides_request"),
),
(misc_msg(|m| m.set_refresh_video(true)), None),
(misc_msg(|m| m.set_refresh_video_display(0)), None),
(

View File

@@ -1023,12 +1023,40 @@ fn get_encoder_config(
},
keyframe_interval,
}),
CodecFormat::AV1 => EncoderCfg::AOM(AomEncoderConfig {
width: c.width as _,
height: c.height as _,
quality,
keyframe_interval,
}),
CodecFormat::AV1 => {
let aom = EncoderCfg::AOM(AomEncoderConfig {
width: c.width as _,
height: c.height as _,
quality,
keyframe_interval,
});
// Prefer svt-av1 for AV1 encoding, keep aom for i444 (svt-av1 is 4:2:0
// only) and for resolutions svt-av1 cannot handle.
#[cfg(target_pointer_width = "64")]
{
let use_i444 = Encoder::use_i444(&aom);
if scrap::svt_av1::SvtAv1Encoder::support(c.width as _, c.height as _) && !use_i444
{
return EncoderCfg::SVTAV1(scrap::svt_av1::SvtAv1EncoderConfig {
width: c.width as _,
height: c.height as _,
quality,
keyframe_interval,
qp_range: None,
preset: None,
});
}
log::info!(
"AV1 uses aom instead of svt-av1: {}",
if use_i444 {
"i444"
} else {
"unsupported resolution"
}
);
}
aom
}
_ => EncoderCfg::VPX(VpxEncoderConfig {
width: c.width as _,
height: c.height as _,
@@ -1326,6 +1354,9 @@ fn check_qos(
) -> ResultType<()> {
let mut video_qos = VIDEO_QOS.lock().unwrap();
*spf = video_qos.spf();
// encoders whose rate control budgets per frame need the real pacing rate,
// implementations no-op when the value is unchanged
encoder.set_fps(video_qos.fps());
if *ratio != video_qos.ratio() {
*ratio = video_qos.ratio();
if encoder.support_changing_quality() {

View File

@@ -8,6 +8,11 @@
"name": "aom",
"host": false
},
{
"name": "svt-av1",
"host": false,
"platform": "!x86 & !arm32 & !uwp"
},
{
"name": "cpu-features",
"platform": "android"