From ff14f577fe4c22c6a51aac4f9cb9ee3097bb74ba Mon Sep 17 00:00:00 2001 From: Liam Warfield Date: Sat, 24 Jan 2026 13:43:40 -0700 Subject: [PATCH 1/8] Add Platform trait. I did some more thinking about the whole trait/boundary stuff and reallized that we don't need the GUI to handle the generic-ness of a trait object since only 1 platform will ever be used in a binary. What do you think of the following: 1. Define a platform trait 2. Each platform defines a zero-sized struct implementing the trait (ex `WebPlatform`). 3. Create an ifdef'd type alias on those structs: ``` // gui/src/platform/mod.rs #[cfg(feature = "web")] pub type CurrentPlatform = web::WebPlatform; #[cfg(feature = "desktop")] pub type CurrentPlatform = desktop::DesktopPlatform; #[cfg(feature = "mobile")] pub type CurrentPlatform = mobile::MobilePlatform; ``` 4. Add a compile time assertion that `CurrentPlatform` implements `Platform`. Pros: - We don't end up working around async trait objects - We define what functions are needed for a platform - We save a little on binary size by avoiding a fully generic solution. Cons: - The trait does not really do much other than being a collection of functions. - In some ways it seems like what we're currently doing but with extra steps. --- gui/src/effects.rs | 13 ++-- gui/src/imp/desktop.rs | 109 ++++++++++++++++++++++++++++-- gui/src/imp/mobile.rs | 111 ++++++++++++++++++++++++++++-- gui/src/imp/mod.rs | 150 ++++++++++++++++++++++++++++++++++++++++- gui/src/imp/web.rs | 92 ++++++++++++++++++++++++- gui/src/lib.rs | 6 +- 6 files changed, 459 insertions(+), 22 deletions(-) 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; } }); } -- 2.52.0 From 411d923c2aa78a8410590224a491578f4d4ecf79 Mon Sep 17 00:00:00 2001 From: Sam Sartor Date: Sat, 24 Jan 2026 20:16:02 -0700 Subject: [PATCH 2/8] wip improved trait shit --- gui/src/app.rs | 18 ++--- gui/src/effects.rs | 17 ++--- gui/src/imp/desktop.rs | 7 +- gui/src/imp/mobile.rs | 4 +- gui/src/imp/mod.rs | 141 ++++++++++++------------------------ gui/src/imp/native_audio.rs | 3 +- gui/src/imp/web.rs | 84 ++++++++------------- gui/src/lib.rs | 21 ++++-- gui/src/main.rs | 4 +- 9 files changed, 118 insertions(+), 181 deletions(-) diff --git a/gui/src/app.rs b/gui/src/app.rs index d60dad1..a56e647 100644 --- a/gui/src/app.rs +++ b/gui/src/app.rs @@ -6,7 +6,7 @@ use mumble_web2_common::{ClientConfig, ServerStatus}; use ordermap::OrderSet; use std::collections::{HashMap, HashSet}; -use crate::imp; +use crate::imp::{Platform, PlatformInterface as _}; pub type ChannelId = u32; pub type UserId = u32; @@ -690,12 +690,12 @@ pub fn LoginView(config: Resource) -> Element { use_resource(move || async move { let client = reqwest::Client::new(); loop { - *last_status.write_unchecked() = Some(imp::get_status(&client).await); - imp::sleep(std::time::Duration::from_secs_f32(1.0)).await; + *last_status.write_unchecked() = Some(Platform::get_status(&client).await); + Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await; } }); - let mut address_input = use_signal(|| imp::load_server_url()); + let mut address_input = use_signal(|| Platform::load_server_url()); let address = use_memo(move || { if let Some(addr) = address_input() { addr.clone() @@ -706,14 +706,14 @@ pub fn LoginView(config: Resource) -> Element { } }); - let previous_username = imp::load_username(); + let previous_username = Platform::load_username(); let mut username = use_signal(|| previous_username.unwrap_or(String::new())); let do_connect = move |_| { //let _ = set_default_username(&username.read()); - let _ = imp::set_default_username(&username.read()); + let _ = Platform::set_default_username(&username.read()); if config.read().as_ref().is_some_and(|cfg| cfg.any_server) { - imp::set_default_server(&address.read()); + Platform::set_default_server(&address.read()); } net.send(Connect { address: address.read().clone(), @@ -860,13 +860,13 @@ pub fn app() -> Element { use_coroutine(|rx: UnboundedReceiver| super::network_entrypoint(rx)); let config = use_resource(|| async move { - match imp::load_config().await { + match Platform::load_config().await { Ok(config) => config, Err(_) => ClientConfig::default(), } }); - imp::request_permissions(); + Platform::request_permissions(); rsx!( document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" } diff --git a/gui/src/effects.rs b/gui/src/effects.rs index 64c9fe3..aef2713 100644 --- a/gui/src/effects.rs +++ b/gui/src/effects.rs @@ -7,10 +7,7 @@ use std::cell::RefCell; use std::sync::Arc; use tracing::{error, info}; -use crate::imp::{CurrentPlatform, PlatformRuntime, SpawnHandleTrait}; - -/// The spawn handle type for the current platform. -type SpawnHandle = ::SpawnHandle; +use crate::imp::{SpawnHandle, SpawnHandleInterface as _}; static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz"); // TODO: make this user configurable. @@ -35,10 +32,7 @@ enum DenoisingModelState { Availible(Box), } -fn with_denoising_model( - spawn: &SpawnHandle, - func: impl FnOnce(&mut DfTract) -> O, -) -> Option { +fn with_denoising_model(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 // thread) while AudioProcessing itself might change threads whenever. thread_local! { @@ -126,7 +120,12 @@ impl AudioProcessor { } impl AudioProcessor { - pub fn process(&mut self, audio: &[f32], channels: usize, output: &mut Vec) -> TransmitState { + pub fn process( + &mut self, + audio: &[f32], + channels: usize, + output: &mut Vec, + ) -> TransmitState { let mut include_raw = true; if self.denoise { with_denoising_model(&self.spawn, |df| { diff --git a/gui/src/imp/desktop.rs b/gui/src/imp/desktop.rs index 5c49df6..460769d 100644 --- a/gui/src/imp/desktop.rs +++ b/gui/src/imp/desktop.rs @@ -7,7 +7,10 @@ use std::collections::HashMap; use std::future::Future; use std::time::Duration; -use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; +use super::{ + PlatformConfig, PlatformInit, PlatformInterface, PlatformNetwork, PlatformRuntime, + SpawnHandleTrait, +}; pub use super::connect::*; pub use super::native_audio::*; @@ -107,7 +110,7 @@ impl PlatformInit for DesktopPlatform { } } -impl Platform for DesktopPlatform {} +impl PlatformInterface for DesktopPlatform {} fn get_config_path() -> std::path::PathBuf { let strategy = choose_app_strategy(AppStrategyArgs { diff --git a/gui/src/imp/mobile.rs b/gui/src/imp/mobile.rs index b32a409..31a4da5 100644 --- a/gui/src/imp/mobile.rs +++ b/gui/src/imp/mobile.rs @@ -7,7 +7,7 @@ use mumble_web2_common::{ClientConfig, ServerStatus}; use std::future::Future; use std::time::Duration; -use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; +use super::{PlatformInterface, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; pub use super::connect::*; pub use super::native_audio::*; @@ -107,7 +107,7 @@ impl PlatformInit for MobilePlatform { } } -impl Platform for MobilePlatform {} +impl PlatformInterface 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 ae20e12..44b29ba 100644 --- a/gui/src/imp/mod.rs +++ b/gui/src/imp/mod.rs @@ -15,8 +15,7 @@ use std::time::Duration; // ============================================================================ /// Trait for spawn handles that can be stored and used to spawn tasks later. -#[cfg(feature = "web")] -pub trait SpawnHandleTrait: Clone + 'static { +pub trait SpawnHandleInterface: Clone + 'static { /// Spawn an async task using this handle. fn spawn(&self, future: F) where @@ -26,51 +25,39 @@ pub trait SpawnHandleTrait: Clone + 'static { 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; +pub trait AudioSystemInterface { + type AudioPlayer: AudioPlayerInterface; } -/// Runtime primitives: task spawning and async sleep. -#[cfg(feature = "web")] -pub trait PlatformRuntime { - /// The spawn handle type for this platform. - type SpawnHandle: SpawnHandleTrait; +pub trait AudioPlayerInterface {} - /// Spawn an async task. - fn spawn(future: F) - where - F: Future + 'static; +/// 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 PlatformInterface { + type AudioSystem: AudioSystemInterface; + type SpawnHandle: SpawnHandleInterface; - /// Async sleep for the given duration. - fn sleep(duration: Duration) -> impl Future; -} + /// Initialize logging for the platform. + fn init_logging(); -/// 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; + /// Request runtime permissions (Android audio recording, etc.). + fn request_permissions(); - /// Spawn an async task. - fn spawn(future: F) - where - F: Future + Send + 'static; + /// 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>; - /// Async sleep for the given duration. - fn sleep(duration: Duration) -> impl Future; -} + /// Get server status (user count, version, etc.). + fn get_status( + client: &reqwest::Client, + ) -> impl Future>; -/// Configuration persistence: loading and saving user preferences. -pub trait PlatformConfig { - /// Load the client configuration (proxy URL, cert hash, etc.). + /// Load the proxy overrides (proxy URL, cert hash, etc.). fn load_config() -> impl Future>; /// Load saved username. @@ -84,39 +71,16 @@ pub trait PlatformConfig { /// 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; } -/// 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 // ============================================================================ @@ -138,36 +102,21 @@ mod mobile; // Platform Type Alias // ============================================================================ -/// The current platform type, selected at compile time based on features. #[cfg(feature = "web")] -pub type CurrentPlatform = web::WebPlatform; +pub type Platform = web::WebPlatform; -#[cfg(feature = "desktop")] -pub type CurrentPlatform = desktop::DesktopPlatform; +#[cfg(all(feature = "desktop", not(feature = "web")))] +pub type Platform = desktop::DesktopPlatform; -#[cfg(feature = "mobile")] -pub type CurrentPlatform = mobile::MobilePlatform; +#[cfg(all(feature = "mobile", not(feature = "web"), not(feature = "desktop")))] +pub type Platform = mobile::MobilePlatform; + +pub type AudioSystem = ::AudioSystem; +pub type AudioPlayer = ::AudioPlayer; +pub type SpawnHandle = ::SpawnHandle; /// Compile-time assertion that CurrentPlatform implements Platform. const _: () = { - fn assert_platform() {} - let _ = assert_platform::; + fn assert_platform() {} + let _ = assert_platform::; }; - -// ============================================================================ -// Platform Re-exports -// ============================================================================ - -#[cfg(feature = "desktop")] -pub use desktop::*; -#[cfg(feature = "mobile")] -pub use mobile::*; - -#[cfg(feature = "mobile")] -pub use mobile::request_permissions; - -#[cfg(any(feature = "desktop", feature = "web"))] -pub fn request_permissions() {} - -#[cfg(all(feature = "web", not(any(feature = "desktop", feature = "mobile"))))] -pub use web::*; diff --git a/gui/src/imp/native_audio.rs b/gui/src/imp/native_audio.rs index 3778aac..0ac62f6 100644 --- a/gui/src/imp/native_audio.rs +++ b/gui/src/imp/native_audio.rs @@ -124,7 +124,8 @@ impl AudioSystem { if let Some(new_processor) = processors.take() { current_processor = new_processor; } - let state = current_processor.process(frame, config.channels as usize, &mut output_buffer); + let state = + current_processor.process(frame, config.channels as usize, &mut output_buffer); encode_and_send(state, &mut output_buffer, &mut encoder, &mut each); }; diff --git a/gui/src/imp/web.rs b/gui/src/imp/web.rs index 575d083..4bea43d 100644 --- a/gui/src/imp/web.rs +++ b/gui/src/imp/web.rs @@ -38,8 +38,6 @@ 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 {} @@ -55,15 +53,14 @@ impl ImpWrite for T {} /// 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 { +#[derive(Clone)] +pub struct WebSpawnHandle; + +impl super::SpawnHandleInterface for WebSpawnHandle { fn spawn(&self, future: F) where F: Future + 'static, @@ -72,26 +69,22 @@ impl SpawnHandleTrait for SpawnHandle { } fn current() -> Self { - SpawnHandle + WebSpawnHandle } } -impl PlatformRuntime for WebPlatform { - type SpawnHandle = SpawnHandle; +impl super::PlatformInterface for WebPlatform { + type AudioSystem = WebAudioSystem; + type SpawnHandle = WebSpawnHandle; - fn spawn(future: F) - where - F: Future + 'static, - { - wasm_bindgen_futures::spawn_local(future); + fn init_logging() { + init_logging(); } - async fn sleep(duration: Duration) { - TimeoutFuture::new(duration.as_millis() as u32).await; + fn request_permissions() { + // No-op on web } -} -impl PlatformConfig for WebPlatform { async fn load_config() -> color_eyre::Result { load_config().await } @@ -111,9 +104,7 @@ impl PlatformConfig for WebPlatform { fn set_default_server(server: &str) -> Option<()> { set_default_server(server) } -} -impl PlatformNetwork for WebPlatform { async fn network_connect( address: String, username: String, @@ -126,20 +117,19 @@ impl PlatformNetwork for WebPlatform { async fn get_status(client: &reqwest::Client) -> color_eyre::Result { get_status(client).await } -} -impl PlatformInit for WebPlatform { - fn init_logging() { - init_logging(); + fn spawn(future: F) + where + F: Future + 'static, + { + wasm_bindgen_futures::spawn_local(future); } - fn request_permissions() { - // No-op on web + async fn sleep(duration: Duration) { + TimeoutFuture::new(duration.as_millis() as u32).await; } } -impl Platform for WebPlatform {} - trait ResultExt { fn ey(self) -> Result; } @@ -162,7 +152,7 @@ impl ResultExt for Result { } } -pub struct AudioSystem { +pub struct WebAudioSystem { webctx: AudioContext, processors: AudioProcessorSender, } @@ -193,7 +183,11 @@ async fn attach_worklet(audio_context: &AudioContext) -> Result<(), Error> { Ok(()) } -impl AudioSystem { +impl super::AudioSystemInterface for WebAudioSystem { + type AudioPlayer = WebAudioPlayer; +} + +impl WebAudioSystem { pub async fn new() -> Result { // Create MediaStreams to playback decoded audio // The audio context is used to reproduce audio. @@ -202,7 +196,7 @@ impl AudioSystem { let processors = AudioProcessorSender::default(); - Ok(AudioSystem { webctx, processors }) + Ok(WebAudioSystem { webctx, processors }) } pub fn set_processor(&self, processor: AudioProcessor) { @@ -224,7 +218,7 @@ impl AudioSystem { Ok(()) } - pub fn create_player(&mut self) -> Result { + pub fn create_player(&mut self) -> Result { let sink_node = AudioWorkletNode::new(&self.webctx, "rust_speaker_worklet").ey()?; // Connect worklet to destination @@ -277,13 +271,15 @@ impl AudioSystem { decoder_error.forget(); output.forget(); - Ok(AudioPlayer(audio_decoder)) + Ok(WebAudioPlayer(audio_decoder)) } } -pub struct AudioPlayer(AudioDecoder); +pub struct WebAudioPlayer(AudioDecoder); -impl AudioPlayer { +impl super::AudioPlayerInterface for WebAudioPlayer {} + +impl WebAudioPlayer { pub fn play_opus(&mut self, payload: &[u8]) { let js_audio_payload = Uint8Array::from(payload); let _ = self.0.decode( @@ -583,19 +579,3 @@ pub fn init_logging() { info!("logging initiated"); } - -#[derive(Clone)] -pub struct SpawnHandle; - -impl SpawnHandle { - pub fn current() -> Self { - SpawnHandle - } - - pub fn spawn(&self, future: F) - where - F: Future + 'static, - { - spawn(future); - } -} diff --git a/gui/src/lib.rs b/gui/src/lib.rs index e37c8a7..a4b5ec1 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -7,11 +7,12 @@ use asynchronous_codec::FramedWrite; use color_eyre::eyre::{bail, Error}; use dioxus::prelude::*; use futures::select; +use futures::AsyncRead; +use futures::AsyncWrite; use futures::FutureExt as _; use futures::SinkExt as _; use futures::StreamExt as _; use futures_channel::mpsc::UnboundedSender; -pub use imp::spawn; use msghtml::process_message_html; use mumble_protocol::control::msgs; use mumble_protocol::control::ControlCodec; @@ -27,7 +28,9 @@ use tracing::error; use tracing::info; use crate::effects::AudioProcessor; -use crate::imp::{AudioSystem, CurrentPlatform, Platform, PlatformNetwork, PlatformRuntime}; +use crate::imp::{ + AudioPlayer, AudioSystem, AudioSystemInterface as _, Platform, PlatformInterface as _, +}; pub mod app; mod effects; @@ -47,7 +50,9 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver) { *STATE.server.write() = Default::default(); *STATE.status.write() = ConnectionState::Connecting; - if let Err(error) = CurrentPlatform::network_connect(address, username, &mut event_rx, &config).await { + if let Err(error) = + Platform::network_connect(address, username, &mut event_rx, &config).await + { error!("could not connect {:?}", error); *STATE.status.write() = ConnectionState::Failed(error.to_string()); } else { @@ -56,7 +61,7 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver) { } } -pub async fn network_loop( +pub async fn network_loop( username: String, event_rx: &mut UnboundedReceiver, mut reader: FramedRead>, @@ -105,12 +110,12 @@ pub async fn network_loop( break; } - CurrentPlatform::sleep(Duration::from_millis(3000)).await; + Platform::sleep(Duration::from_millis(3000)).await; } }); } - let mut audio = imp::AudioSystem::new().await?; + let mut audio = AudioSystem::new().await?; { let send_chan = send_chan.clone(); let mut sequence_num = 0; @@ -296,8 +301,8 @@ fn accept_command( fn accept_packet( msg: ControlPacket, - audio_context: &mut imp::AudioSystem, - player_map: &mut HashMap, + audio_context: &mut AudioSystem, + player_map: &mut HashMap, ) -> Result<(), Error> { match msg { ControlPacket::UDPTunnel(u) => { diff --git a/gui/src/main.rs b/gui/src/main.rs index 0e2f19f..a4185c6 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -1,6 +1,6 @@ -use mumble_web2_gui::{app, imp}; +use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _}; pub fn main() { - imp::init_logging(); + Platform::init_logging(); dioxus::launch(app::app); } -- 2.52.0 From 056a673bc091d3c58ec9ca2d54d8b3cdfa1b61e8 Mon Sep 17 00:00:00 2001 From: Sam Sartor Date: Sat, 24 Jan 2026 21:11:58 -0700 Subject: [PATCH 3/8] all platform traits implemented & dumb async runtime imports --- gui/src/effects.rs | 2 +- gui/src/imp/desktop.rs | 136 ++++++++---------------------------- gui/src/imp/mobile.rs | 135 +++++++++-------------------------- gui/src/imp/mod.rs | 33 ++++----- gui/src/imp/native_audio.rs | 33 +++++---- gui/src/imp/web.rs | 52 ++++++-------- 6 files changed, 120 insertions(+), 271 deletions(-) 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; } -- 2.52.0 From 09985e6031090756dad18f1de70d9ff33b1ab9b0 Mon Sep 17 00:00:00 2001 From: Sam Sartor Date: Sat, 24 Jan 2026 21:19:58 -0700 Subject: [PATCH 4/8] move audio stuff into trait --- gui/src/imp/mod.rs | 31 +++++++++++++++---- gui/src/imp/native_audio.rs | 60 ++++++++++++++++++------------------- gui/src/imp/web.rs | 19 ++++-------- gui/src/lib.rs | 3 +- 4 files changed, 63 insertions(+), 50 deletions(-) diff --git a/gui/src/imp/mod.rs b/gui/src/imp/mod.rs index 9618d33..0b6cfe6 100644 --- a/gui/src/imp/mod.rs +++ b/gui/src/imp/mod.rs @@ -2,8 +2,9 @@ //! //! This module defines traits that each platform (web, desktop, mobile) must implement. //! The traits make the platform boundary explicit and provide compile-time verification. +#![allow(async_fn_in_trait)] -use crate::app::Command; +use crate::{app::Command, effects::AudioProcessor}; use color_eyre::eyre::Error; use dioxus::hooks::UnboundedReceiver; use mumble_web2_common::{ClientConfig, ServerStatus}; @@ -14,11 +15,31 @@ use std::time::Duration; // Trait Definitions // ============================================================================ -pub trait AudioSystemInterface { +pub trait AudioSystemInterface: Sized { type AudioPlayer: AudioPlayerInterface; + + /// Initialize the audio system, including relevant state + async fn new() -> Result; + + /// Set the processor for the microphone input, mainly noise cancellation settings. + fn set_processor(&self, processor: AudioProcessor); + + /// Begin listening to microphone input, calling the `each` function with + /// encoded opus frames. + fn start_recording( + &mut self, + each: impl FnMut(Vec, bool) + Send + 'static, + ) -> Result<(), Error>; + + /// Begin playback of an audio stream, returning an object that can be passed + /// with opus frames. + fn create_player(&mut self) -> Result; } -pub trait AudioPlayerInterface {} +pub trait AudioPlayerInterface { + /// Playback an opus frame. + fn play_opus(&mut self, payload: &[u8]); +} /// This is the main trait that each platform must implement. It combines all /// platform-specific functionality into a single interface, providing compile-time @@ -72,9 +93,9 @@ pub trait PlatformInterface { pub mod web; #[cfg(any(feature = "desktop", feature = "mobile"))] -mod connect; +pub mod connect; #[cfg(any(feature = "desktop", feature = "mobile"))] -mod native_audio; +pub mod native_audio; #[cfg(feature = "desktop")] pub mod desktop; diff --git a/gui/src/imp/native_audio.rs b/gui/src/imp/native_audio.rs index 0c9670f..d8141d0 100644 --- a/gui/src/imp/native_audio.rs +++ b/gui/src/imp/native_audio.rs @@ -56,27 +56,6 @@ fn encode_and_send( type Buffer = Arc>>>; impl NativeAudioSystem { - pub async fn new() -> Result { - // TODO - let host = cpal::default_host(); - let name = host.id(); - let processors = AudioProcessorSender::default(); - Ok(NativeAudioSystem { - output: host - .default_output_device() - .ok_or(eyre!("no output devices from {name:?}"))?, - input: host - .default_input_device() - .ok_or(eyre!("no input devices from {name:?}"))?, - processors, - recording_stream: None, - }) - } - - pub fn set_processor(&self, processor: AudioProcessor) { - self.processors.store(Some(processor)) - } - fn choose_config( &self, configs: impl Iterator, @@ -106,8 +85,33 @@ impl NativeAudioSystem { .cloned() .ok_or(eyre!("no supported stream configs")) } +} - pub fn start_recording( +impl super::AudioSystemInterface for NativeAudioSystem { + type AudioPlayer = NativeAudioPlayer; + + async fn new() -> Result { + // TODO + let host = cpal::default_host(); + let name = host.id(); + let processors = AudioProcessorSender::default(); + Ok(NativeAudioSystem { + output: host + .default_output_device() + .ok_or(eyre!("no output devices from {name:?}"))?, + input: host + .default_input_device() + .ok_or(eyre!("no input devices from {name:?}"))?, + processors, + recording_stream: None, + }) + } + + fn set_processor(&self, processor: AudioProcessor) { + self.processors.store(Some(processor)) + } + + fn start_recording( &mut self, mut each: impl FnMut(Vec, bool) + Send + 'static, ) -> Result<(), Error> { @@ -148,7 +152,7 @@ impl NativeAudioSystem { } } - pub fn create_player(&mut self) -> Result { + fn create_player(&mut self) -> Result { let config = self.choose_config(self.output.supported_output_configs()?)?; info!( "creating player on {:?} with {:#?}", @@ -195,10 +199,6 @@ impl NativeAudioSystem { } } -impl super::AudioSystemInterface for NativeAudioSystem { - type AudioPlayer = NativeAudioPlayer; -} - pub struct NativeAudioPlayer { decoder: opus::Decoder, stream: cpal::Stream, @@ -206,8 +206,8 @@ pub struct NativeAudioPlayer { tmp: Vec, } -impl NativeAudioPlayer { - pub fn play_opus(&mut self, payload: &[u8]) { +impl super::AudioPlayerInterface for NativeAudioPlayer { + fn play_opus(&mut self, payload: &[u8]) { let len = match self.decoder.decode(payload, &mut self.tmp, false) { Ok(l) => l, Err(e) => { @@ -228,5 +228,3 @@ impl NativeAudioPlayer { } } } - -impl super::AudioPlayerInterface for NativeAudioPlayer {} diff --git a/gui/src/imp/web.rs b/gui/src/imp/web.rs index c198e98..1bfef8f 100644 --- a/gui/src/imp/web.rs +++ b/gui/src/imp/web.rs @@ -177,10 +177,8 @@ async fn attach_worklet(audio_context: &AudioContext) -> Result<(), Error> { impl super::AudioSystemInterface for WebAudioSystem { type AudioPlayer = WebAudioPlayer; -} -impl WebAudioSystem { - pub async fn new() -> Result { + async fn new() -> Result { // Create MediaStreams to playback decoded audio // The audio context is used to reproduce audio. let webctx = configure_audio_context(); @@ -191,14 +189,11 @@ impl WebAudioSystem { Ok(WebAudioSystem { webctx, processors }) } - pub fn set_processor(&self, processor: AudioProcessor) { + fn set_processor(&self, processor: AudioProcessor) { self.processors.store(Some(processor)) } - pub fn start_recording( - &mut self, - each: impl FnMut(Vec, bool) + 'static, - ) -> Result<(), Error> { + fn start_recording(&mut self, each: impl FnMut(Vec, bool) + 'static) -> Result<(), Error> { let audio_context_worklet = self.webctx.clone(); let processors = self.processors.clone(); spawn(async move { @@ -210,7 +205,7 @@ impl WebAudioSystem { Ok(()) } - pub fn create_player(&mut self) -> Result { + fn create_player(&mut self) -> Result { let sink_node = AudioWorkletNode::new(&self.webctx, "rust_speaker_worklet").ey()?; // Connect worklet to destination @@ -269,10 +264,8 @@ impl WebAudioSystem { pub struct WebAudioPlayer(AudioDecoder); -impl super::AudioPlayerInterface for WebAudioPlayer {} - -impl WebAudioPlayer { - pub fn play_opus(&mut self, payload: &[u8]) { +impl super::AudioPlayerInterface for WebAudioPlayer { + fn play_opus(&mut self, payload: &[u8]) { let js_audio_payload = Uint8Array::from(payload); let _ = self.0.decode( &EncodedAudioChunk::new(&EncodedAudioChunkInit::new( diff --git a/gui/src/lib.rs b/gui/src/lib.rs index a4b5ec1..bb875b2 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -29,7 +29,8 @@ use tracing::info; use crate::effects::AudioProcessor; use crate::imp::{ - AudioPlayer, AudioSystem, AudioSystemInterface as _, Platform, PlatformInterface as _, + AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _, Platform, + PlatformInterface as _, }; pub mod app; -- 2.52.0 From 35b2a06e64d70765923d1b6579db21c4449ce978 Mon Sep 17 00:00:00 2001 From: Sam Sartor Date: Sat, 24 Jan 2026 22:08:09 -0700 Subject: [PATCH 5/8] some ideas including stub --- gui/Cargo.toml | 1 - gui/src/imp/connect.rs | 5 ++ gui/src/imp/mobile.rs | 4 -- gui/src/imp/mod.rs | 50 ++++++++++----- gui/src/imp/native_audio.rs | 10 --- gui/src/imp/stub.rs | 117 ++++++++++++++++++++++++++++++++++++ gui/src/imp/web.rs | 16 +---- 7 files changed, 160 insertions(+), 43 deletions(-) create mode 100644 gui/src/imp/stub.rs diff --git a/gui/Cargo.toml b/gui/Cargo.toml index 0433118..7e29572 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -146,7 +146,6 @@ desktop = [ "rfd/xdg-portal", "etcetera", ] - mobile = [ "dioxus/mobile", "tokio", diff --git a/gui/src/imp/connect.rs b/gui/src/imp/connect.rs index 8136e2f..e7d3ce7 100644 --- a/gui/src/imp/connect.rs +++ b/gui/src/imp/connect.rs @@ -108,3 +108,8 @@ pub async fn network_connect( pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result { bail!("status not supported on desktop yet") } + +#[allow(unused)] +pub use tokio::spawn; +#[allow(unused)] +pub type SpawnHandle = tokio::runtime::Handle; diff --git a/gui/src/imp/mobile.rs b/gui/src/imp/mobile.rs index 5f887c2..dec58d8 100644 --- a/gui/src/imp/mobile.rs +++ b/gui/src/imp/mobile.rs @@ -7,10 +7,6 @@ use std::time::Duration; pub use super::connect::*; -// ============================================================================ -// Platform Struct -// ============================================================================ - /// Mobile platform implementation using Tokio, native audio, and Android permissions. pub struct MobilePlatform; diff --git a/gui/src/imp/mod.rs b/gui/src/imp/mod.rs index 0b6cfe6..9952d85 100644 --- a/gui/src/imp/mod.rs +++ b/gui/src/imp/mod.rs @@ -31,8 +31,7 @@ pub trait AudioSystemInterface: Sized { each: impl FnMut(Vec, bool) + Send + 'static, ) -> Result<(), Error>; - /// Begin playback of an audio stream, returning an object that can be passed - /// with opus frames. + /// Begin playback of an audio stream, returning an object that can be passed opus frames. fn create_player(&mut self) -> Result; } @@ -89,18 +88,17 @@ pub trait PlatformInterface { // Platform Modules // ============================================================================ -#[cfg(feature = "web")] -pub mod web; - #[cfg(any(feature = "desktop", feature = "mobile"))] -pub mod connect; -#[cfg(any(feature = "desktop", feature = "mobile"))] -pub mod native_audio; - +mod connect; #[cfg(feature = "desktop")] -pub mod desktop; +mod desktop; #[cfg(feature = "mobile")] -pub mod mobile; +mod mobile; +#[cfg(any(feature = "desktop", feature = "mobile"))] +mod native_audio; +mod stub; +#[cfg(feature = "web")] +mod web; // ============================================================================ // Platform Type Alias @@ -115,20 +113,44 @@ pub type Platform = desktop::DesktopPlatform; #[cfg(all(feature = "mobile", not(feature = "web"), not(feature = "desktop")))] pub type Platform = mobile::MobilePlatform; +#[cfg(all( + not(feature = "mobile"), + not(feature = "web"), + not(feature = "desktop") +))] +pub type Platform = stub::StubPlatform; + pub type AudioSystem = ::AudioSystem; pub type AudioPlayer = ::AudioPlayer; // ======================== // 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}; +pub use connect::{spawn, SpawnHandle}; +#[cfg(all( + not(feature = "desktop"), + not(feature = "mobile"), + not(feature = "web") +))] +pub use stub::{spawn, SpawnHandle}; #[cfg(feature = "web")] pub use web::{spawn, SpawnHandle}; -/// Compile-time assertion that CurrentPlatform implements Platform. +// ======================= +// Compile-time Assertions +// ======================= const _: () = { fn assert_platform() {} - let _ = assert_platform::; + + // Check each implementation, and prevent warnings that the implementations are unused. + #[cfg(feature = "web")] + let _ = assert_platform::; + #[cfg(feature = "desktop")] + let _ = assert_platform::; + #[cfg(feature = "mobile")] + let _ = assert_platform::; + let _ = assert_platform::; }; diff --git a/gui/src/imp/native_audio.rs b/gui/src/imp/native_audio.rs index d8141d0..5b5b3c5 100644 --- a/gui/src/imp/native_audio.rs +++ b/gui/src/imp/native_audio.rs @@ -6,16 +6,6 @@ use std::sync::Arc; use std::sync::Mutex; use tracing::{error, info, warn}; -// ============= -// Async runtime -// ============= -pub use tokio::spawn; -pub type SpawnHandle = tokio::runtime::Handle; - -// ============ -// Audio System -// ============ - pub struct NativeAudioSystem { output: cpal::Device, input: cpal::Device, diff --git a/gui/src/imp/stub.rs b/gui/src/imp/stub.rs new file mode 100644 index 0000000..c4f2917 --- /dev/null +++ b/gui/src/imp/stub.rs @@ -0,0 +1,117 @@ +use crate::effects::AudioProcessor; +use color_eyre::eyre::Error; +use dioxus::hooks::UnboundedReceiver; +use mumble_web2_common::{ClientConfig, ServerStatus}; +use std::future::Future; + +pub struct StubPlatform; + +impl super::PlatformInterface for StubPlatform { + type AudioSystem = StubAudioSystem; + + fn init_logging() { + todo!() + } + + fn request_permissions() { + todo!() + } + + fn network_connect( + _address: String, + _username: String, + _event_rx: &mut UnboundedReceiver, + _gui_config: &ClientConfig, + ) -> impl Future> { + async { todo!() } + } + + fn get_status( + _client: &reqwest::Client, + ) -> impl Future> { + async { todo!() } + } + + fn load_config() -> impl Future> { + async { todo!() } + } + + fn load_username() -> Option { + todo!() + } + + fn load_server_url() -> Option { + todo!() + } + + fn set_default_username(_username: &str) -> Option<()> { + todo!() + } + + fn set_default_server(_server: &str) -> Option<()> { + todo!() + } + + fn sleep(_duration: std::time::Duration) -> impl Future { + async { todo!() } + } +} + +pub struct StubAudioSystem; + +impl super::AudioSystemInterface for StubAudioSystem { + type AudioPlayer = StubAudioPlayer; + + async fn new() -> Result { + todo!() + } + + fn set_processor(&self, _processor: AudioProcessor) { + todo!() + } + + fn start_recording( + &mut self, + _each: impl FnMut(Vec, bool) + Send + 'static, + ) -> Result<(), Error> { + todo!() + } + + fn create_player(&mut self) -> Result { + todo!() + } +} + +pub struct StubAudioPlayer; + +impl super::AudioPlayerInterface for StubAudioPlayer { + fn play_opus(&mut self, _payload: &[u8]) { + todo!() + } +} + +#[allow(unused)] +pub struct SpawnHandle; + +impl SpawnHandle { + #[allow(unused)] + pub fn spawn(&self, _future: F) + where + F: Future + 'static, + { + todo!() + } + + #[allow(unused)] + pub fn current() -> Self { + SpawnHandle + } +} + +#[allow(unused)] +pub fn spawn(_future: F) +where + F: Future + 'static, +{ + todo!() +} diff --git a/gui/src/imp/web.rs b/gui/src/imp/web.rs index 1bfef8f..92a1a18 100644 --- a/gui/src/imp/web.rs +++ b/gui/src/imp/web.rs @@ -38,18 +38,10 @@ use web_sys::WorkletOptions; use web_sys::{console, window}; use web_sys::{AudioContext, AudioDataCopyToOptions}; -pub trait ImpRead: AsyncRead + Unpin + 'static {} -impl ImpRead for T {} - -pub trait ImpWrite: AsyncWrite + Unpin + 'static {} -impl ImpWrite for T {} - -// ============= -// Async runtime -// ============= - +#[allow(unused)] pub use wasm_bindgen_futures::spawn_local as spawn; +#[allow(unused)] #[derive(Clone)] pub struct SpawnHandle; @@ -66,10 +58,6 @@ impl SpawnHandle { } } -// ============================================================================ -// Platform Struct -// ============================================================================ - /// Web platform implementation using WebTransport and Web Audio API. pub struct WebPlatform; -- 2.52.0 From 2e86f68a3ccc1544fa1530519a15a28b9b93035e Mon Sep 17 00:00:00 2001 From: Sam Sartor Date: Sat, 24 Jan 2026 22:16:16 -0700 Subject: [PATCH 6/8] some more review changes --- gui/src/imp/native_audio.rs | 1 - gui/src/imp/stub.rs | 36 +++++----- gui/src/imp/web.rs | 131 ++++++++++++++---------------------- 3 files changed, 70 insertions(+), 98 deletions(-) diff --git a/gui/src/imp/native_audio.rs b/gui/src/imp/native_audio.rs index 5b5b3c5..ed3eb43 100644 --- a/gui/src/imp/native_audio.rs +++ b/gui/src/imp/native_audio.rs @@ -81,7 +81,6 @@ impl super::AudioSystemInterface for NativeAudioSystem { type AudioPlayer = NativeAudioPlayer; async fn new() -> Result { - // TODO let host = cpal::default_host(); let name = host.id(); let processors = AudioProcessorSender::default(); diff --git a/gui/src/imp/stub.rs b/gui/src/imp/stub.rs index c4f2917..d03cd5e 100644 --- a/gui/src/imp/stub.rs +++ b/gui/src/imp/stub.rs @@ -1,3 +1,5 @@ +/// Stub implementation of the platform interface, so that we can +/// `cargo check` without any --feature flags. use crate::effects::AudioProcessor; use color_eyre::eyre::Error; use dioxus::hooks::UnboundedReceiver; @@ -10,11 +12,11 @@ impl super::PlatformInterface for StubPlatform { type AudioSystem = StubAudioSystem; fn init_logging() { - todo!() + panic!("stubbed platform") } fn request_permissions() { - todo!() + panic!("stubbed platform") } fn network_connect( @@ -23,37 +25,37 @@ impl super::PlatformInterface for StubPlatform { _event_rx: &mut UnboundedReceiver, _gui_config: &ClientConfig, ) -> impl Future> { - async { todo!() } + async { panic!("stubbed platform") } } fn get_status( _client: &reqwest::Client, ) -> impl Future> { - async { todo!() } + async { panic!("stubbed platform") } } fn load_config() -> impl Future> { - async { todo!() } + async { panic!("stubbed platform") } } fn load_username() -> Option { - todo!() + panic!("stubbed platform") } fn load_server_url() -> Option { - todo!() + panic!("stubbed platform") } fn set_default_username(_username: &str) -> Option<()> { - todo!() + panic!("stubbed platform") } fn set_default_server(_server: &str) -> Option<()> { - todo!() + panic!("stubbed platform") } fn sleep(_duration: std::time::Duration) -> impl Future { - async { todo!() } + async { panic!("stubbed platform") } } } @@ -63,22 +65,22 @@ impl super::AudioSystemInterface for StubAudioSystem { type AudioPlayer = StubAudioPlayer; async fn new() -> Result { - todo!() + panic!("stubbed platform") } fn set_processor(&self, _processor: AudioProcessor) { - todo!() + panic!("stubbed platform") } fn start_recording( &mut self, _each: impl FnMut(Vec, bool) + Send + 'static, ) -> Result<(), Error> { - todo!() + panic!("stubbed platform") } fn create_player(&mut self) -> Result { - todo!() + panic!("stubbed platform") } } @@ -86,7 +88,7 @@ pub struct StubAudioPlayer; impl super::AudioPlayerInterface for StubAudioPlayer { fn play_opus(&mut self, _payload: &[u8]) { - todo!() + panic!("stubbed platform") } } @@ -99,7 +101,7 @@ impl SpawnHandle { where F: Future + 'static, { - todo!() + panic!("stubbed platform") } #[allow(unused)] @@ -113,5 +115,5 @@ pub fn spawn(_future: F) where F: Future + 'static, { - todo!() + panic!("stubbed platform") } diff --git a/gui/src/imp/web.rs b/gui/src/imp/web.rs index 92a1a18..1a55ec4 100644 --- a/gui/src/imp/web.rs +++ b/gui/src/imp/web.rs @@ -3,7 +3,6 @@ use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState}; use color_eyre::eyre::{bail, eyre, Error}; use crossbeam::atomic::AtomicCell; use dioxus::prelude::*; -use futures::{AsyncRead, AsyncWrite}; use gloo_timers::future::TimeoutFuture; use js_sys::Float32Array; use mumble_protocol::control::ClientControlCodec; @@ -65,7 +64,25 @@ impl super::PlatformInterface for WebPlatform { type AudioSystem = WebAudioSystem; fn init_logging() { - init_logging(); + // copied from tracing_web example usage + + use tracing_subscriber::fmt::format::Pretty; + use tracing_subscriber::prelude::*; + use tracing_web::{performance_layer, MakeWebConsoleWriter}; + + let fmt_layer = tracing_subscriber::fmt::layer() + .with_ansi(false) // Only partially supported across browsers + .without_time() // std::time is not available in browsers + .with_writer(MakeWebConsoleWriter::new()) // write events to the console + .with_filter(LevelFilter::DEBUG); + let perf_layer = performance_layer().with_details_from_fields(Pretty::default()); + + tracing_subscriber::registry() + .with(fmt_layer) + .with(perf_layer) + .init(); + + info!("logging initiated"); } fn request_permissions() { @@ -73,23 +90,43 @@ impl super::PlatformInterface for WebPlatform { } async fn load_config() -> color_eyre::Result { - load_config().await + let config_url = match option_env!("MUMBLE_WEB2_GUI_CONFIG_URL") { + Some(url) => Url::parse(url)?, + None => absolute_url("config")?, + }; + info!("loading config from {}", config_url); + + let config = reqwest::get(config_url) + .await? + .json::() + .await?; + + Ok(config) } fn load_username() -> Option { - load_username() + web_sys::window() + .unwrap() + .local_storage() + .ok()?? + .get_item("username") + .ok()? } fn load_server_url() -> Option { - load_server_url() + None } fn set_default_username(username: &str) -> Option<()> { - set_default_username(username) + web_sys::window()? + .local_storage() + .ok()?? + .set_item("username", username) + .ok() } - fn set_default_server(server: &str) -> Option<()> { - set_default_server(server) + fn set_default_server(_server: &str) -> Option<()> { + None } async fn network_connect( @@ -102,7 +139,12 @@ impl super::PlatformInterface for WebPlatform { } async fn get_status(client: &reqwest::Client) -> color_eyre::Result { - get_status(client).await + Ok(client + .get(absolute_url("status")?) + .send() + .await? + .json::() + .await?) } async fn sleep(duration: Duration) { @@ -476,79 +518,8 @@ pub async fn network_connect( crate::network_loop(username, event_rx, reader, writer).await } -pub fn set_default_username(username: &str) -> Option<()> { - web_sys::window()? - .local_storage() - .ok()?? - .set_item("username", username) - .ok() -} - -pub fn set_default_server(username: &str) -> Option<()> { - None -} - -pub fn load_username() -> Option { - web_sys::window() - .unwrap() - .local_storage() - .ok()?? - .get_item("username") - .ok()? -} - -pub fn load_server_url() -> Option { - None -} - pub fn absolute_url(path: &str) -> Result { let window: web_sys::Window = web_sys::window().expect("no global `window` exists"); let location = window.location(); Ok(Url::parse(&location.href().ey()?)?.join(path)?) } - -pub async fn load_config() -> color_eyre::Result { - let config_url = match option_env!("MUMBLE_WEB2_GUI_CONFIG_URL") { - Some(url) => Url::parse(url)?, - None => absolute_url("config")?, - }; - info!("loading config from {}", config_url); - - let config = reqwest::get(config_url) - .await? - .json::() - .await?; - - Ok(config) -} - -pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result { - Ok(client - .get(absolute_url("status")?) - .send() - .await? - .json::() - .await?) -} - -pub fn init_logging() { - // copied from tracing_web example usage - - use tracing_subscriber::fmt::format::Pretty; - use tracing_subscriber::prelude::*; - use tracing_web::{performance_layer, MakeWebConsoleWriter}; - - let fmt_layer = tracing_subscriber::fmt::layer() - .with_ansi(false) // Only partially supported across browsers - .without_time() // std::time is not available in browsers - .with_writer(MakeWebConsoleWriter::new()) // write events to the console - .with_filter(LevelFilter::DEBUG); - let perf_layer = performance_layer().with_details_from_fields(Pretty::default()); - - tracing_subscriber::registry() - .with(fmt_layer) - .with(perf_layer) - .init(); - - info!("logging initiated"); -} -- 2.52.0 From 817038327827a38720e50a0336cd5831bf0cd0d6 Mon Sep 17 00:00:00 2001 From: Liam Warfield Date: Sun, 25 Jan 2026 10:08:56 -0700 Subject: [PATCH 7/8] Remove connection::* import. --- gui/src/imp/mobile.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gui/src/imp/mobile.rs b/gui/src/imp/mobile.rs index dec58d8..cbdf085 100644 --- a/gui/src/imp/mobile.rs +++ b/gui/src/imp/mobile.rs @@ -5,8 +5,6 @@ use mumble_web2_common::{ClientConfig, ServerStatus}; use std::future::Future; use std::time::Duration; -pub use super::connect::*; - /// Mobile platform implementation using Tokio, native audio, and Android permissions. pub struct MobilePlatform; @@ -43,7 +41,7 @@ impl super::PlatformInterface for MobilePlatform { 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 { -- 2.52.0 From 5f3466546ed8334c9167a66d3e71a1a79ff22c7a Mon Sep 17 00:00:00 2001 From: Liam Warfield Date: Sun, 25 Jan 2026 11:08:21 -0700 Subject: [PATCH 8/8] Add some Trait doc comments Added comments to Audio(System|Player)Interface traits. --- gui/src/imp/desktop.rs | 4 ---- gui/src/imp/mobile.rs | 2 +- gui/src/imp/mod.rs | 14 ++++++++++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/gui/src/imp/desktop.rs b/gui/src/imp/desktop.rs index a8553a3..34a59f8 100644 --- a/gui/src/imp/desktop.rs +++ b/gui/src/imp/desktop.rs @@ -6,10 +6,6 @@ use mumble_web2_common::{ClientConfig, ServerStatus}; use std::collections::HashMap; use std::time::Duration; -// ============================================================================ -// Platform Struct -// ============================================================================ - /// Desktop platform implementation using Tokio and native audio. pub struct DesktopPlatform; diff --git a/gui/src/imp/mobile.rs b/gui/src/imp/mobile.rs index cbdf085..cac7b86 100644 --- a/gui/src/imp/mobile.rs +++ b/gui/src/imp/mobile.rs @@ -45,7 +45,7 @@ impl super::PlatformInterface for MobilePlatform { } async fn get_status(client: &reqwest::Client) -> color_eyre::Result { - get_status(client).await + super::connect::get_status(client).await } fn init_logging() { diff --git a/gui/src/imp/mod.rs b/gui/src/imp/mod.rs index 9952d85..a657d31 100644 --- a/gui/src/imp/mod.rs +++ b/gui/src/imp/mod.rs @@ -15,10 +15,15 @@ use std::time::Duration; // Trait Definitions // ============================================================================ +/// Platform-specific audio subsystem for capturing microphone input and creating playback streams. +/// +/// The audio system handles Opus encoding internally - callers receive encoded frames +/// ready for network transmission. pub trait AudioSystemInterface: Sized { + /// The player type returned by [`create_player`](Self::create_player). type AudioPlayer: AudioPlayerInterface; - /// Initialize the audio system, including relevant state + /// Initialize the audio system. async fn new() -> Result; /// Set the processor for the microphone input, mainly noise cancellation settings. @@ -35,8 +40,13 @@ pub trait AudioSystemInterface: Sized { fn create_player(&mut self) -> Result; } +/// A handle to an active audio playback stream for a single remote user. +/// +/// Each connected user gets their own `AudioPlayer` instance, which decodes +/// incoming Opus frames and outputs PCM audio to the platform's audio device. +/// The player manages its own decoder state and output buffer. pub trait AudioPlayerInterface { - /// Playback an opus frame. + /// Decode and play an Opus-encoded audio frame. fn play_opus(&mut self, payload: &[u8]); } -- 2.52.0