diff --git a/gui/src/effects.rs b/gui/src/effects.rs index aef2713..58f3abe 100644 --- a/gui/src/effects.rs +++ b/gui/src/effects.rs @@ -7,7 +7,7 @@ use std::cell::RefCell; use std::sync::Arc; use tracing::{error, info}; -use crate::imp::{SpawnHandle, SpawnHandleInterface as _}; +use crate::imp::SpawnHandle; static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz"); // TODO: make this user configurable. diff --git a/gui/src/imp/desktop.rs b/gui/src/imp/desktop.rs index 460769d..a8553a3 100644 --- a/gui/src/imp/desktop.rs +++ b/gui/src/imp/desktop.rs @@ -4,20 +4,8 @@ use dioxus::hooks::UnboundedReceiver; use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs}; use mumble_web2_common::{ClientConfig, ServerStatus}; use std::collections::HashMap; -use std::future::Future; use std::time::Duration; -use super::{ - PlatformConfig, PlatformInit, PlatformInterface, PlatformNetwork, PlatformRuntime, - SpawnHandleTrait, -}; - -pub use super::connect::*; -pub use super::native_audio::*; - -pub use tokio::task::spawn; -pub use tokio::time::sleep; - // ============================================================================ // Platform Struct // ============================================================================ @@ -25,84 +13,69 @@ pub use tokio::time::sleep; /// Desktop platform implementation using Tokio and native audio. pub struct DesktopPlatform; -// ============================================================================ -// SpawnHandle -// ============================================================================ - -pub type SpawnHandle = tokio::runtime::Handle; - -impl SpawnHandleTrait for SpawnHandle { - fn spawn(&self, future: F) - where - F: Future + Send + 'static, - { - let _ = tokio::runtime::Handle::spawn(self, future); - } - - fn current() -> Self { - tokio::runtime::Handle::current() - } -} - -// ============================================================================ -// Trait Implementations -// ============================================================================ - -impl PlatformRuntime for DesktopPlatform { - type SpawnHandle = SpawnHandle; - - fn spawn(future: F) - where - F: Future + Send + 'static, - { - let _ = tokio::task::spawn(future); - } +impl super::PlatformInterface for DesktopPlatform { + type AudioSystem = super::native_audio::NativeAudioSystem; async fn sleep(duration: Duration) { tokio::time::sleep(duration).await; } -} -impl PlatformConfig for DesktopPlatform { async fn load_config() -> color_eyre::Result { - load_config().await + Ok(ClientConfig { + proxy_url: None, + cert_hash: None, + any_server: true, + }) } fn load_username() -> Option { - load_username() + let config = load_config_map(); + config.get("username").cloned() } fn load_server_url() -> Option { - load_server_url() + let config = load_config_map(); + config.get("server").cloned() } fn set_default_username(username: &str) -> Option<()> { - set_default_username(username) + let mut config = load_config_map(); + config.insert("username".to_string(), username.to_string()); + save_config_map(&config).ok() } fn set_default_server(server: &str) -> Option<()> { - set_default_server(server) + let mut config = load_config_map(); + config.insert("server".to_string(), server.to_string()); + save_config_map(&config).ok() } -} -impl PlatformNetwork for DesktopPlatform { async fn network_connect( address: String, username: String, event_rx: &mut UnboundedReceiver, gui_config: &ClientConfig, ) -> Result<(), Error> { - network_connect(address, username, event_rx, gui_config).await + super::connect::network_connect(address, username, event_rx, gui_config).await } async fn get_status(client: &reqwest::Client) -> color_eyre::Result { - get_status(client).await + super::connect::get_status(client).await } -} -impl PlatformInit for DesktopPlatform { fn init_logging() { - init_logging(); + use tracing::level_filters::LevelFilter; + use tracing_subscriber::filter::EnvFilter; + + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + + tracing_subscriber::fmt() + .with_target(true) + .with_level(true) + .with_env_filter(env_filter) + .init(); } fn request_permissions() { @@ -110,8 +83,6 @@ impl PlatformInit for DesktopPlatform { } } -impl PlatformInterface for DesktopPlatform {} - fn get_config_path() -> std::path::PathBuf { let strategy = choose_app_strategy(AppStrategyArgs { top_level_domain: "com".to_string(), @@ -139,48 +110,3 @@ fn save_config_map(config: &HashMap) -> color_eyre::Result<()> { std::fs::write(&config_path, contents)?; Ok(()) } - -pub fn set_default_username(username: &str) -> Option<()> { - let mut config = load_config_map(); - config.insert("username".to_string(), username.to_string()); - save_config_map(&config).ok() -} - -pub fn set_default_server(server: &str) -> Option<()> { - let mut config = load_config_map(); - config.insert("server".to_string(), server.to_string()); - save_config_map(&config).ok() -} - -pub fn load_username() -> Option { - let config = load_config_map(); - config.get("username").cloned() -} - -pub fn load_server_url() -> Option { - let config = load_config_map(); - config.get("server").cloned() -} - -pub async fn load_config() -> color_eyre::Result { - Ok(ClientConfig { - proxy_url: None, - cert_hash: None, - any_server: true, - }) -} - -pub fn init_logging() { - use tracing::level_filters::LevelFilter; - use tracing_subscriber::filter::EnvFilter; - - let env_filter = EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .from_env_lossy(); - - tracing_subscriber::fmt() - .with_target(true) - .with_level(true) - .with_env_filter(env_filter) - .init(); -} diff --git a/gui/src/imp/mobile.rs b/gui/src/imp/mobile.rs index 31a4da5..5f887c2 100644 --- a/gui/src/imp/mobile.rs +++ b/gui/src/imp/mobile.rs @@ -1,19 +1,11 @@ -use android_permissions::{PermissionManager, RECORD_AUDIO}; use crate::app::Command; use color_eyre::eyre::Error; use dioxus::hooks::UnboundedReceiver; -use jni::{objects::JObject, JavaVM}; use mumble_web2_common::{ClientConfig, ServerStatus}; use std::future::Future; use std::time::Duration; -use super::{PlatformInterface, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; - pub use super::connect::*; -pub use super::native_audio::*; - -pub use tokio::task::spawn; -pub use tokio::time::sleep; // ============================================================================ // Platform Struct @@ -22,67 +14,33 @@ pub use tokio::time::sleep; /// Mobile platform implementation using Tokio, native audio, and Android permissions. pub struct MobilePlatform; -// ============================================================================ -// SpawnHandle -// ============================================================================ +impl super::PlatformInterface for MobilePlatform { + type AudioSystem = super::native_audio::NativeAudioSystem; -pub type SpawnHandle = tokio::runtime::Handle; - -impl SpawnHandleTrait for SpawnHandle { - fn spawn(&self, future: F) - where - F: Future + Send + 'static, - { - let _ = tokio::runtime::Handle::spawn(self, future); - } - - fn current() -> Self { - tokio::runtime::Handle::current() - } -} - -// ============================================================================ -// Trait Implementations -// ============================================================================ - -impl PlatformRuntime for MobilePlatform { - type SpawnHandle = SpawnHandle; - - fn spawn(future: F) - where - F: Future + Send + 'static, - { - let _ = tokio::task::spawn(future); - } - - async fn sleep(duration: Duration) { - tokio::time::sleep(duration).await; - } -} - -impl PlatformConfig for MobilePlatform { async fn load_config() -> color_eyre::Result { - load_config().await + Ok(ClientConfig { + proxy_url: None, + cert_hash: None, + any_server: true, + }) } fn load_username() -> Option { - load_username() + None } fn load_server_url() -> Option { - load_server_url() + None } - fn set_default_username(username: &str) -> Option<()> { - set_default_username(username) + fn set_default_username(_username: &str) -> Option<()> { + None } fn set_default_server(server: &str) -> Option<()> { - set_default_server(server) + None } -} -impl PlatformNetwork for MobilePlatform { async fn network_connect( address: String, username: String, @@ -95,66 +53,39 @@ impl PlatformNetwork for MobilePlatform { async fn get_status(client: &reqwest::Client) -> color_eyre::Result { get_status(client).await } -} -impl PlatformInit for MobilePlatform { fn init_logging() { - init_logging(); + use tracing::level_filters::LevelFilter; + use tracing_subscriber::filter::EnvFilter; + + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + + tracing_subscriber::fmt() + .with_target(true) + .with_level(true) + .with_env_filter(env_filter) + .init(); } fn request_permissions() { request_recording_permission(); } + + async fn sleep(duration: Duration) { + tokio::time::sleep(duration).await; + } } -impl PlatformInterface for MobilePlatform {} - -pub fn set_default_username(_username: &str) -> Option<()> { - None -} - -pub fn set_default_server(server: &str) -> Option<()> { - None -} - -pub fn load_username() -> Option { - None -} - -pub fn load_server_url() -> Option { - None -} - -pub async fn load_config() -> color_eyre::Result { - Ok(ClientConfig { - proxy_url: None, - cert_hash: None, - any_server: true, - }) -} - -pub fn init_logging() { - use tracing::level_filters::LevelFilter; - use tracing_subscriber::filter::EnvFilter; - - let env_filter = EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .from_env_lossy(); - - tracing_subscriber::fmt() - .with_target(true) - .with_level(true) - .with_env_filter(env_filter) - .init(); -} - -#[cfg(feature = "mobile")] -pub fn request_permissions() { - request_recording_permission(); -} +#[cfg(not(target_os = "android"))] +pub fn request_recording_permission() {} #[cfg(target_os = "android")] pub fn request_recording_permission() { + use android_permissions::{PermissionManager, RECORD_AUDIO}; + use jni::{objects::JObject, JavaVM}; + let ctx = ndk_context::android_context(); let vm = unsafe { JavaVM::from_raw(ctx.vm().cast()).unwrap() }; let activity = unsafe { JObject::from_raw(ctx.context().cast()) }; diff --git a/gui/src/imp/mod.rs b/gui/src/imp/mod.rs index 44b29ba..9618d33 100644 --- a/gui/src/imp/mod.rs +++ b/gui/src/imp/mod.rs @@ -14,17 +14,6 @@ use std::time::Duration; // Trait Definitions // ============================================================================ -/// Trait for spawn handles that can be stored and used to spawn tasks later. -pub trait SpawnHandleInterface: Clone + 'static { - /// Spawn an async task using this handle. - fn spawn(&self, future: F) - where - F: Future + 'static; - - /// Get a spawn handle for the current context. - fn current() -> Self; -} - pub trait AudioSystemInterface { type AudioPlayer: AudioPlayerInterface; } @@ -36,7 +25,6 @@ pub trait AudioPlayerInterface {} /// verification that all platforms implement the required functionality. pub trait PlatformInterface { type AudioSystem: AudioSystemInterface; - type SpawnHandle: SpawnHandleInterface; /// Initialize logging for the platform. fn init_logging(); @@ -72,11 +60,6 @@ pub trait PlatformInterface { /// Save the default server URL. fn set_default_server(server: &str) -> Option<()>; - /// Spawn an async task. - fn spawn(future: F) - where - F: Future + 'static; - /// Async sleep for the given duration. fn sleep(duration: Duration) -> impl Future; } @@ -86,7 +69,7 @@ pub trait PlatformInterface { // ============================================================================ #[cfg(feature = "web")] -mod web; +pub mod web; #[cfg(any(feature = "desktop", feature = "mobile"))] mod connect; @@ -94,9 +77,9 @@ mod connect; mod native_audio; #[cfg(feature = "desktop")] -mod desktop; +pub mod desktop; #[cfg(feature = "mobile")] -mod mobile; +pub mod mobile; // ============================================================================ // Platform Type Alias @@ -113,7 +96,15 @@ pub type Platform = mobile::MobilePlatform; pub type AudioSystem = ::AudioSystem; pub type AudioPlayer = ::AudioPlayer; -pub type SpawnHandle = ::SpawnHandle; + +// ======================== +// Platform Async Runtime +// ======================== +// Note: these can not be part of the Platform because they differ in Send requiremets +#[cfg(all(any(feature = "desktop", feature = "mobile"), not(feature = "web")))] +pub use native_audio::{spawn, SpawnHandle}; +#[cfg(feature = "web")] +pub use web::{spawn, SpawnHandle}; /// Compile-time assertion that CurrentPlatform implements Platform. const _: () = { diff --git a/gui/src/imp/native_audio.rs b/gui/src/imp/native_audio.rs index 0ac62f6..0c9670f 100644 --- a/gui/src/imp/native_audio.rs +++ b/gui/src/imp/native_audio.rs @@ -1,19 +1,22 @@ use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState}; use color_eyre::eyre::{eyre, Error}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _}; -use futures::io::{AsyncRead, AsyncWrite}; use std::mem::replace; use std::sync::Arc; use std::sync::Mutex; use tracing::{error, info, warn}; -pub trait ImpRead: AsyncRead + Unpin + Send + 'static {} -impl ImpRead for T {} +// ============= +// Async runtime +// ============= +pub use tokio::spawn; +pub type SpawnHandle = tokio::runtime::Handle; -pub trait ImpWrite: AsyncWrite + Unpin + Send + 'static {} -impl ImpWrite for T {} +// ============ +// Audio System +// ============ -pub struct AudioSystem { +pub struct NativeAudioSystem { output: cpal::Device, input: cpal::Device, processors: AudioProcessorSender, @@ -52,13 +55,13 @@ fn encode_and_send( type Buffer = Arc>>>; -impl AudioSystem { +impl NativeAudioSystem { pub async fn new() -> Result { // TODO let host = cpal::default_host(); let name = host.id(); let processors = AudioProcessorSender::default(); - Ok(AudioSystem { + Ok(NativeAudioSystem { output: host .default_output_device() .ok_or(eyre!("no output devices from {name:?}"))?, @@ -145,7 +148,7 @@ impl AudioSystem { } } - pub fn create_player(&mut self) -> Result { + pub fn create_player(&mut self) -> Result { let config = self.choose_config(self.output.supported_output_configs()?)?; info!( "creating player on {:?} with {:#?}", @@ -183,7 +186,7 @@ impl AudioSystem { )? }; stream.play()?; - Ok(AudioPlayer { + Ok(NativeAudioPlayer { decoder, stream, buffer, @@ -192,14 +195,18 @@ impl AudioSystem { } } -pub struct AudioPlayer { +impl super::AudioSystemInterface for NativeAudioSystem { + type AudioPlayer = NativeAudioPlayer; +} + +pub struct NativeAudioPlayer { decoder: opus::Decoder, stream: cpal::Stream, buffer: Buffer, tmp: Vec, } -impl AudioPlayer { +impl NativeAudioPlayer { pub fn play_opus(&mut self, payload: &[u8]) { let len = match self.decoder.decode(payload, &mut self.tmp, false) { Ok(l) => l, @@ -221,3 +228,5 @@ impl AudioPlayer { } } } + +impl super::AudioPlayerInterface for NativeAudioPlayer {} diff --git a/gui/src/imp/web.rs b/gui/src/imp/web.rs index 4bea43d..c198e98 100644 --- a/gui/src/imp/web.rs +++ b/gui/src/imp/web.rs @@ -38,14 +38,34 @@ use web_sys::WorkletOptions; use web_sys::{console, window}; use web_sys::{AudioContext, AudioDataCopyToOptions}; -pub use wasm_bindgen_futures::spawn_local as spawn; - pub trait ImpRead: AsyncRead + Unpin + 'static {} impl ImpRead for T {} pub trait ImpWrite: AsyncWrite + Unpin + 'static {} impl ImpWrite for T {} +// ============= +// Async runtime +// ============= + +pub use wasm_bindgen_futures::spawn_local as spawn; + +#[derive(Clone)] +pub struct SpawnHandle; + +impl SpawnHandle { + pub fn spawn(&self, future: F) + where + F: Future + 'static, + { + wasm_bindgen_futures::spawn_local(future); + } + + pub fn current() -> Self { + SpawnHandle + } +} + // ============================================================================ // Platform Struct // ============================================================================ @@ -53,29 +73,8 @@ impl ImpWrite for T {} /// Web platform implementation using WebTransport and Web Audio API. pub struct WebPlatform; -// ============================================================================ -// Trait Implementations -// ============================================================================ - -#[derive(Clone)] -pub struct WebSpawnHandle; - -impl super::SpawnHandleInterface for WebSpawnHandle { - fn spawn(&self, future: F) - where - F: Future + 'static, - { - wasm_bindgen_futures::spawn_local(future); - } - - fn current() -> Self { - WebSpawnHandle - } -} - impl super::PlatformInterface for WebPlatform { type AudioSystem = WebAudioSystem; - type SpawnHandle = WebSpawnHandle; fn init_logging() { init_logging(); @@ -118,13 +117,6 @@ impl super::PlatformInterface for WebPlatform { get_status(client).await } - fn spawn(future: F) - where - F: Future + 'static, - { - wasm_bindgen_futures::spawn_local(future); - } - async fn sleep(duration: Duration) { TimeoutFuture::new(duration.as_millis() as u32).await; }