From 0973f51df9bf1f68c1e6f09f5204c8f7471dc2ff Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 23 Nov 2024 18:41:27 +0800 Subject: [PATCH] feat: macos, audio, loopback (#10025) Signed-off-by: fufesou --- .github/workflows/flutter-build.yml | 12 ++++- Cargo.lock | 22 +++++++-- Cargo.toml | 3 +- build.py | 9 ++++ flutter/lib/common/widgets/audio_input.dart | 8 ++-- src/common.rs | 2 +- src/flutter_ffi.rs | 17 ++++--- src/server/audio_service.rs | 51 ++++++++++++++++++++- src/ui_interface.rs | 2 + 9 files changed, 105 insertions(+), 21 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index f132d43ac..22b555764 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -20,6 +20,7 @@ on: env: SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81 CARGO_NDK_VERSION: "3.1.2" SCITER_ARMV7_CMAKE_VERSION: "3.29.7" SCITER_NASM_DEBVERSION: "2.14-1" @@ -641,7 +642,7 @@ jobs: target: aarch64-apple-darwin, os: macos-latest, # extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296 - extra-build-args: "", + extra-build-args: "--screencapturekit", arch: aarch64, } steps: @@ -720,7 +721,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: - toolchain: ${{ env.RUST_VERSION }} + toolchain: ${{ env.MAC_RUST_VERSION }} targets: ${{ matrix.job.target }} components: "rustfmt" @@ -767,6 +768,13 @@ jobs: - name: Build rustdesk run: | + if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then + MIN_MACOS_VERSION="12.3" + sed -i -e "s/MACOSX_DEPLOYMENT_TARGET\=[0-9]*.[0-9]*/MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}/" build.py + sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile + sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml + sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj + fi ./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }} - name: create unsigned dmg diff --git a/Cargo.lock b/Cargo.lock index 9b450437d..6e2e56b3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -893,6 +893,20 @@ dependencies = [ "regex", ] +[[package]] +name = "cidre" +version = "0.4.0" +source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf" +dependencies = [ + "cidre-macros", + "parking_lot", +] + +[[package]] +name = "cidre-macros" +version = "0.1.0" +source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf" + [[package]] name = "cipher" version = "0.4.4" @@ -1276,10 +1290,10 @@ dependencies = [ [[package]] name = "cpal" version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#4d318ff778063ce14669fd4bd67a1673653fc6e5" dependencies = [ "alsa", + "cidre", "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", "coreaudio-rs", "dasp_sample", @@ -3505,7 +3519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if 1.0.0", - "windows-targets 0.52.5", + "windows-targets 0.48.5", ] [[package]] @@ -4149,7 +4163,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 1.3.1", "proc-macro2 1.0.86", "quote 1.0.36", "syn 2.0.68", diff --git a/Cargo.toml b/Cargo.toml index eca0fc55c..dbf819bf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ unix-file-copy-paste = [ "dep:once_cell", "clipboard/unix-file-copy-paste", ] +screencapturekit = ["cpal/screencapturekit"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -77,7 +78,7 @@ fon = "0.6" zip = "0.6" shutdown_hooks = "0.1" totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } -cpal = "0.15" +cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" } ringbuf = "0.3" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] diff --git a/build.py b/build.py index b7ea0a1ef..174bd18eb 100755 --- a/build.py +++ b/build.py @@ -143,6 +143,12 @@ def make_parser(): "--package", type=str ) + if osx: + parser.add_argument( + '--screencapturekit', + action='store_true', + help='Enable feature screencapturekit' + ) return parser @@ -274,6 +280,9 @@ def get_features(args): features.append('flutter') if args.unix_file_copy_paste: features.append('unix-file-copy-paste') + if osx: + if args.screencapturekit: + features.append('screencapturekit') print("features:", features) return features diff --git a/flutter/lib/common/widgets/audio_input.dart b/flutter/lib/common/widgets/audio_input.dart index 1db439127..1f8f1a8b9 100644 --- a/flutter/lib/common/widgets/audio_input.dart +++ b/flutter/lib/common/widgets/audio_input.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/platform_model.dart'; -const _kWindowsSystemSound = 'System Sound'; +const _kSystemSound = 'System Sound'; typedef AudioINputSetDevice = void Function(String device); typedef AudioInputBuilder = Widget Function( @@ -21,7 +21,7 @@ class AudioInput extends StatelessWidget { : super(key: key); static String getDefault() { - if (isWindows) return translate('System Sound'); + if (bind.mainAudioSupportLoopback()) return translate(_kSystemSound); return ''; } @@ -55,8 +55,8 @@ class AudioInput extends StatelessWidget { static Future> getDevicesInfo( bool isCm, bool isVoiceCall) async { List devices = (await bind.mainGetSoundInputs()).toList(); - if (isWindows) { - devices.insert(0, translate(_kWindowsSystemSound)); + if (bind.mainAudioSupportLoopback()) { + devices.insert(0, translate(_kSystemSound)); } String current = await getValue(isCm, isVoiceCall); return {'devices': devices, 'current': current}; diff --git a/src/common.rs b/src/common.rs index f53dd703f..b1f97e27b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -837,7 +837,7 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data); } } - *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; + *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; } Ok(()) } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ba03bc761..90dafccd0 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -774,13 +774,6 @@ pub fn main_get_sound_inputs() -> Vec { vec![String::from("")] } -pub fn main_get_default_sound_input() -> Option { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - return get_default_sound_input(); - #[cfg(any(target_os = "android", target_os = "ios"))] - None -} - pub fn main_get_login_device_info() -> SyncReturn { SyncReturn(get_login_device_info_json()) } @@ -2317,6 +2310,16 @@ pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usi } } +pub fn main_audio_support_loopback() -> SyncReturn { + #[cfg(target_os = "windows")] + let is_surpport = true; + #[cfg(feature = "screencapturekit")] + let is_surpport = crate::audio_service::is_screen_capture_kit_available(); + #[cfg(not(any(target_os = "windows", feature = "screencapturekit")))] + let is_surpport = false; + SyncReturn(is_surpport) +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs index 5d60abf6c..d1bb2d878 100644 --- a/src/server/audio_service.rs +++ b/src/server/audio_service.rs @@ -156,6 +156,14 @@ mod pa_impl { } } +#[inline] +#[cfg(feature = "screencapturekit")] +pub fn is_screen_capture_kit_available() -> bool { + cpal::available_hosts() + .iter() + .any(|host| *host == cpal::HostId::ScreenCaptureKit) +} + #[cfg(not(any(target_os = "linux", target_os = "android")))] mod cpal_impl { use self::service::{Reset, ServiceSwap}; @@ -170,6 +178,11 @@ mod cpal_impl { static ref INPUT_BUFFER: Arc>> = Default::default(); } + #[cfg(feature = "screencapturekit")] + lazy_static::lazy_static! { + static ref HOST_SCREEN_CAPTURE_KIT: Result = cpal::host_from_id(cpal::HostId::ScreenCaptureKit); + } + #[derive(Default)] pub struct State { stream: Option<(Box, Arc)>, @@ -246,6 +259,27 @@ mod cpal_impl { send_f32(&data, encoder, sp); } + #[cfg(feature = "screencapturekit")] + fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { + let audio_input = super::get_audio_input(); + if !audio_input.is_empty() { + return get_audio_input(&audio_input); + } + if !is_screen_capture_kit_available() { + return get_audio_input(""); + } + let device = HOST_SCREEN_CAPTURE_KIT + .as_ref()? + .default_input_device() + .with_context(|| "Failed to get default input device for loopback")?; + let format = device + .default_input_config() + .map_err(|e| anyhow!(e)) + .with_context(|| "Failed to get input output format")?; + log::info!("Default input format: {:?}", format); + Ok((device, format)) + } + #[cfg(windows)] fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { let audio_input = super::get_audio_input(); @@ -267,7 +301,7 @@ mod cpal_impl { Ok((device, format)) } - #[cfg(not(windows))] + #[cfg(not(any(windows, feature = "screencapturekit")))] fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { let audio_input = super::get_audio_input(); get_audio_input(&audio_input) @@ -275,7 +309,20 @@ mod cpal_impl { fn get_audio_input(audio_input: &str) -> ResultType<(Device, SupportedStreamConfig)> { let mut device = None; - if !audio_input.is_empty() { + #[cfg(feature = "screencapturekit")] + if !audio_input.is_empty() && is_screen_capture_kit_available() { + for d in HOST_SCREEN_CAPTURE_KIT + .as_ref()? + .devices() + .with_context(|| "Failed to get audio devices")? + { + if d.name().unwrap_or("".to_owned()) == audio_input { + device = Some(d); + break; + } + } + } + if device.is_none() && !audio_input.is_empty() { for d in HOST .devices() .with_context(|| "Failed to get audio devices")? diff --git a/src/ui_interface.rs b/src/ui_interface.rs index bab54c79a..caff46b84 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -321,6 +321,8 @@ pub fn get_sound_inputs() -> Vec { fn get_sound_inputs_() -> Vec { let mut out = Vec::new(); use cpal::traits::{DeviceTrait, HostTrait}; + // Do not use `cpal::host_from_id(cpal::HostId::ScreenCaptureKit)` for feature = "screencapturekit" + // Because we explicitly handle the "System Sound" device. let host = cpal::default_host(); if let Ok(devices) = host.devices() { for device in devices {