diff --git a/gui/src/effects.rs b/gui/src/effects.rs index ef87bcd..64c9fe3 100644 --- a/gui/src/effects.rs +++ b/gui/src/effects.rs @@ -7,7 +7,10 @@ use std::cell::RefCell; use std::sync::Arc; use tracing::{error, info}; -use crate::imp; +use crate::imp::{CurrentPlatform, PlatformRuntime, SpawnHandleTrait}; + +/// The spawn handle type for the current platform. +type SpawnHandle = ::SpawnHandle; static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz"); // TODO: make this user configurable. @@ -33,7 +36,7 @@ enum DenoisingModelState { } fn with_denoising_model( - spawn: &imp::SpawnHandle, + spawn: &SpawnHandle, func: impl FnOnce(&mut DfTract) -> O, ) -> Option { // Using a thread local is super gross, but DfTract is not Send (so it can never leave the current @@ -89,7 +92,7 @@ fn with_denoising_model( pub struct AudioProcessor { denoise: bool, - spawn: imp::SpawnHandle, + spawn: SpawnHandle, buffer: Vec, noise_floor: f32, /// Whether we were transmitting in the previous frame @@ -102,7 +105,7 @@ impl AudioProcessor { pub fn new_plain() -> Self { AudioProcessor { denoise: false, - spawn: imp::SpawnHandle::current(), + spawn: SpawnHandle::current(), buffer: Vec::new(), noise_floor: DEFAULT_NOISE_FLOOR, was_transmitting: false, @@ -113,7 +116,7 @@ impl AudioProcessor { pub fn new_denoising() -> Self { AudioProcessor { denoise: true, - spawn: imp::SpawnHandle::current(), + spawn: SpawnHandle::current(), buffer: Vec::new(), noise_floor: DEFAULT_NOISE_FLOOR, was_transmitting: false, diff --git a/gui/src/imp/desktop.rs b/gui/src/imp/desktop.rs index 8a41f31..5c49df6 100644 --- a/gui/src/imp/desktop.rs +++ b/gui/src/imp/desktop.rs @@ -1,13 +1,114 @@ +use crate::app::Command; +use color_eyre::eyre::Error; +use dioxus::hooks::UnboundedReceiver; use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs}; -use mumble_web2_common::ClientConfig; +use mumble_web2_common::{ClientConfig, ServerStatus}; use std::collections::HashMap; -pub use tokio::runtime::Handle as SpawnHandle; -pub use tokio::task::spawn; -pub use tokio::time::sleep; +use std::future::Future; +use std::time::Duration; + +use super::{Platform, 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 +// ============================================================================ + +/// 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); + } + + async fn sleep(duration: Duration) { + tokio::time::sleep(duration).await; + } +} + +impl PlatformConfig for DesktopPlatform { + async fn load_config() -> color_eyre::Result { + load_config().await + } + + fn load_username() -> Option { + load_username() + } + + fn load_server_url() -> Option { + load_server_url() + } + + fn set_default_username(username: &str) -> Option<()> { + set_default_username(username) + } + + fn set_default_server(server: &str) -> Option<()> { + set_default_server(server) + } +} + +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 + } + + async fn get_status(client: &reqwest::Client) -> color_eyre::Result { + get_status(client).await + } +} + +impl PlatformInit for DesktopPlatform { + fn init_logging() { + init_logging(); + } + + fn request_permissions() { + // No-op on desktop + } +} + +impl Platform for DesktopPlatform {} + fn get_config_path() -> std::path::PathBuf { let strategy = choose_app_strategy(AppStrategyArgs { top_level_domain: "com".to_string(), diff --git a/gui/src/imp/mobile.rs b/gui/src/imp/mobile.rs index b632934..b32a409 100644 --- a/gui/src/imp/mobile.rs +++ b/gui/src/imp/mobile.rs @@ -1,16 +1,115 @@ 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; +use mumble_web2_common::{ClientConfig, ServerStatus}; +use std::future::Future; +use std::time::Duration; -use std::collections::HashMap; -pub use tokio::runtime::Handle as SpawnHandle; -pub use tokio::task::spawn; -pub use tokio::time::sleep; +use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; pub use super::connect::*; pub use super::native_audio::*; -pub fn set_default_username(username: &str) -> Option<()> { +pub use tokio::task::spawn; +pub use tokio::time::sleep; + +// ============================================================================ +// Platform Struct +// ============================================================================ + +/// Mobile platform implementation using Tokio, native audio, and Android permissions. +pub struct MobilePlatform; + +// ============================================================================ +// 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 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 + } + + fn load_username() -> Option { + load_username() + } + + fn load_server_url() -> Option { + load_server_url() + } + + fn set_default_username(username: &str) -> Option<()> { + set_default_username(username) + } + + fn set_default_server(server: &str) -> Option<()> { + set_default_server(server) + } +} + +impl PlatformNetwork for MobilePlatform { + 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 + } + + async fn get_status(client: &reqwest::Client) -> color_eyre::Result { + get_status(client).await + } +} + +impl PlatformInit for MobilePlatform { + fn init_logging() { + init_logging(); + } + + fn request_permissions() { + request_recording_permission(); + } +} + +impl Platform for MobilePlatform {} + +pub fn set_default_username(_username: &str) -> Option<()> { None } diff --git a/gui/src/imp/mod.rs b/gui/src/imp/mod.rs index 106e281..ae20e12 100644 --- a/gui/src/imp/mod.rs +++ b/gui/src/imp/mod.rs @@ -1,3 +1,126 @@ +//! Platform abstraction layer +//! +//! This module defines traits that each platform (web, desktop, mobile) must implement. +//! The traits make the platform boundary explicit and provide compile-time verification. + +use crate::app::Command; +use color_eyre::eyre::Error; +use dioxus::hooks::UnboundedReceiver; +use mumble_web2_common::{ClientConfig, ServerStatus}; +use std::future::Future; +use std::time::Duration; + +// ============================================================================ +// Trait Definitions +// ============================================================================ + +/// Trait for spawn handles that can be stored and used to spawn tasks later. +#[cfg(feature = "web")] +pub trait SpawnHandleTrait: 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; +} + +/// Trait for spawn handles that can be stored and used to spawn tasks later. +#[cfg(any(feature = "desktop", feature = "mobile"))] +pub trait SpawnHandleTrait: Clone + 'static { + /// Spawn an async task using this handle. + fn spawn(&self, future: F) + where + F: Future + Send + 'static; + + /// Get a spawn handle for the current context. + fn current() -> Self; +} + +/// Runtime primitives: task spawning and async sleep. +#[cfg(feature = "web")] +pub trait PlatformRuntime { + /// The spawn handle type for this platform. + type SpawnHandle: SpawnHandleTrait; + + /// Spawn an async task. + fn spawn(future: F) + where + F: Future + 'static; + + /// Async sleep for the given duration. + fn sleep(duration: Duration) -> impl Future; +} + +/// Runtime primitives: task spawning and async sleep. +#[cfg(any(feature = "desktop", feature = "mobile"))] +pub trait PlatformRuntime { + /// The spawn handle type for this platform. + type SpawnHandle: SpawnHandleTrait; + + /// Spawn an async task. + fn spawn(future: F) + where + F: Future + Send + 'static; + + /// Async sleep for the given duration. + fn sleep(duration: Duration) -> impl Future; +} + +/// Configuration persistence: loading and saving user preferences. +pub trait PlatformConfig { + /// Load the client configuration (proxy URL, cert hash, etc.). + fn load_config() -> impl Future>; + + /// Load saved username. + fn load_username() -> Option; + + /// Load saved server URL. + fn load_server_url() -> Option; + + /// Save the default username. + fn set_default_username(username: &str) -> Option<()>; + + /// Save the default server URL. + fn set_default_server(server: &str) -> Option<()>; +} + +/// Network operations: connecting to servers. +pub trait PlatformNetwork { + /// Establish a connection to the Mumble server and run the network loop. + fn network_connect( + address: String, + username: String, + event_rx: &mut UnboundedReceiver, + gui_config: &ClientConfig, + ) -> impl Future>; + + /// Get server status (user count, version, etc.). + fn get_status(client: &reqwest::Client) + -> impl Future>; +} + +/// Platform initialization. +pub trait PlatformInit { + /// Initialize logging for the platform. + fn init_logging(); + + /// Request runtime permissions (Android audio recording, etc.). + fn request_permissions(); +} + +/// Combined platform trait. +/// +/// This is the main trait that each platform must implement. It combines all +/// platform-specific functionality into a single interface, providing compile-time +/// verification that all platforms implement the required functionality. +pub trait Platform: PlatformRuntime + PlatformConfig + PlatformNetwork + PlatformInit {} + +// ============================================================================ +// Platform Modules +// ============================================================================ + #[cfg(feature = "web")] mod web; @@ -11,6 +134,30 @@ mod desktop; #[cfg(feature = "mobile")] mod mobile; +// ============================================================================ +// Platform Type Alias +// ============================================================================ + +/// The current platform type, selected at compile time based on features. +#[cfg(feature = "web")] +pub type CurrentPlatform = web::WebPlatform; + +#[cfg(feature = "desktop")] +pub type CurrentPlatform = desktop::DesktopPlatform; + +#[cfg(feature = "mobile")] +pub type CurrentPlatform = mobile::MobilePlatform; + +/// Compile-time assertion that CurrentPlatform implements Platform. +const _: () = { + fn assert_platform() {} + let _ = assert_platform::; +}; + +// ============================================================================ +// Platform Re-exports +// ============================================================================ + #[cfg(feature = "desktop")] pub use desktop::*; #[cfg(feature = "mobile")] @@ -24,6 +171,3 @@ pub fn request_permissions() {} #[cfg(all(feature = "web", not(any(feature = "desktop", feature = "mobile"))))] pub use web::*; - -#[cfg(any(feature = "desktop"))] -pub use desktop::*; diff --git a/gui/src/imp/web.rs b/gui/src/imp/web.rs index 4a6339c..575d083 100644 --- a/gui/src/imp/web.rs +++ b/gui/src/imp/web.rs @@ -29,7 +29,6 @@ use web_sys::AudioWorkletNode; use web_sys::EncodedAudioChunk; use web_sys::EncodedAudioChunkInit; use web_sys::EncodedAudioChunkType; -use web_sys::MediaStream; use web_sys::MediaStreamConstraints; use web_sys::MessageEvent; use web_sys::WebTransport; @@ -39,6 +38,8 @@ use web_sys::WorkletOptions; use web_sys::{console, window}; use web_sys::{AudioContext, AudioDataCopyToOptions}; +use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; + pub use wasm_bindgen_futures::spawn_local as spawn; pub trait ImpRead: AsyncRead + Unpin + 'static {} @@ -47,10 +48,98 @@ impl ImpRead for T {} pub trait ImpWrite: AsyncWrite + Unpin + 'static {} impl ImpWrite for T {} +// ============================================================================ +// Platform Struct +// ============================================================================ + +/// Web platform implementation using WebTransport and Web Audio API. +pub struct WebPlatform; + pub async fn sleep(d: Duration) { TimeoutFuture::new(d.as_millis() as u32).await } +// ============================================================================ +// Trait Implementations +// ============================================================================ + +impl SpawnHandleTrait for SpawnHandle { + fn spawn(&self, future: F) + where + F: Future + 'static, + { + wasm_bindgen_futures::spawn_local(future); + } + + fn current() -> Self { + SpawnHandle + } +} + +impl PlatformRuntime for WebPlatform { + type SpawnHandle = SpawnHandle; + + 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; + } +} + +impl PlatformConfig for WebPlatform { + async fn load_config() -> color_eyre::Result { + load_config().await + } + + fn load_username() -> Option { + load_username() + } + + fn load_server_url() -> Option { + load_server_url() + } + + fn set_default_username(username: &str) -> Option<()> { + set_default_username(username) + } + + fn set_default_server(server: &str) -> Option<()> { + set_default_server(server) + } +} + +impl PlatformNetwork for WebPlatform { + 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 + } + + async fn get_status(client: &reqwest::Client) -> color_eyre::Result { + get_status(client).await + } +} + +impl PlatformInit for WebPlatform { + fn init_logging() { + init_logging(); + } + + fn request_permissions() { + // No-op on web + } +} + +impl Platform for WebPlatform {} + trait ResultExt { fn ey(self) -> Result; } @@ -495,6 +584,7 @@ pub fn init_logging() { info!("logging initiated"); } +#[derive(Clone)] pub struct SpawnHandle; impl SpawnHandle { diff --git a/gui/src/lib.rs b/gui/src/lib.rs index b44e5fc..e37c8a7 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -27,7 +27,7 @@ use tracing::error; use tracing::info; use crate::effects::AudioProcessor; -use crate::imp::AudioSystem; +use crate::imp::{AudioSystem, CurrentPlatform, Platform, PlatformNetwork, PlatformRuntime}; pub mod app; mod effects; @@ -47,7 +47,7 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver) { *STATE.server.write() = Default::default(); *STATE.status.write() = ConnectionState::Connecting; - if let Err(error) = imp::network_connect(address, username, &mut event_rx, &config).await { + if let Err(error) = CurrentPlatform::network_connect(address, username, &mut event_rx, &config).await { error!("could not connect {:?}", error); *STATE.status.write() = ConnectionState::Failed(error.to_string()); } else { @@ -105,7 +105,7 @@ pub async fn network_loop( break; } - imp::sleep(Duration::from_millis(3000)).await; + CurrentPlatform::sleep(Duration::from_millis(3000)).await; } }); }