all platform traits implemented & dumb async runtime imports

This commit is contained in:
2026-01-24 21:11:58 -07:00
committed by Liam Warfield
parent 411d923c2a
commit 056a673bc0
6 changed files with 120 additions and 271 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ use std::cell::RefCell;
use std::sync::Arc; use std::sync::Arc;
use tracing::{error, info}; 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"); static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz");
// TODO: make this user configurable. // TODO: make this user configurable.
+31 -105
View File
@@ -4,20 +4,8 @@ use dioxus::hooks::UnboundedReceiver;
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs}; use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
use mumble_web2_common::{ClientConfig, ServerStatus}; use mumble_web2_common::{ClientConfig, ServerStatus};
use std::collections::HashMap; use std::collections::HashMap;
use std::future::Future;
use std::time::Duration; 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 // Platform Struct
// ============================================================================ // ============================================================================
@@ -25,84 +13,69 @@ pub use tokio::time::sleep;
/// Desktop platform implementation using Tokio and native audio. /// Desktop platform implementation using Tokio and native audio.
pub struct DesktopPlatform; pub struct DesktopPlatform;
// ============================================================================ impl super::PlatformInterface for DesktopPlatform {
// SpawnHandle type AudioSystem = super::native_audio::NativeAudioSystem;
// ============================================================================
pub type SpawnHandle = tokio::runtime::Handle;
impl SpawnHandleTrait for SpawnHandle {
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + 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<F>(future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let _ = tokio::task::spawn(future);
}
async fn sleep(duration: Duration) { async fn sleep(duration: Duration) {
tokio::time::sleep(duration).await; tokio::time::sleep(duration).await;
} }
}
impl PlatformConfig for DesktopPlatform {
async fn load_config() -> color_eyre::Result<ClientConfig> { async fn load_config() -> color_eyre::Result<ClientConfig> {
load_config().await Ok(ClientConfig {
proxy_url: None,
cert_hash: None,
any_server: true,
})
} }
fn load_username() -> Option<String> { fn load_username() -> Option<String> {
load_username() let config = load_config_map();
config.get("username").cloned()
} }
fn load_server_url() -> Option<String> { fn load_server_url() -> Option<String> {
load_server_url() let config = load_config_map();
config.get("server").cloned()
} }
fn set_default_username(username: &str) -> Option<()> { 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<()> { 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( async fn network_connect(
address: String, address: String,
username: String, username: String,
event_rx: &mut UnboundedReceiver<Command>, event_rx: &mut UnboundedReceiver<Command>,
gui_config: &ClientConfig, gui_config: &ClientConfig,
) -> Result<(), Error> { ) -> 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<ServerStatus> { async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
get_status(client).await super::connect::get_status(client).await
} }
}
impl PlatformInit for DesktopPlatform {
fn init_logging() { 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() { fn request_permissions() {
@@ -110,8 +83,6 @@ impl PlatformInit for DesktopPlatform {
} }
} }
impl PlatformInterface for DesktopPlatform {}
fn get_config_path() -> std::path::PathBuf { fn get_config_path() -> std::path::PathBuf {
let strategy = choose_app_strategy(AppStrategyArgs { let strategy = choose_app_strategy(AppStrategyArgs {
top_level_domain: "com".to_string(), top_level_domain: "com".to_string(),
@@ -139,48 +110,3 @@ fn save_config_map(config: &HashMap<String, String>) -> color_eyre::Result<()> {
std::fs::write(&config_path, contents)?; std::fs::write(&config_path, contents)?;
Ok(()) 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<String> {
let config = load_config_map();
config.get("username").cloned()
}
pub fn load_server_url() -> Option<String> {
let config = load_config_map();
config.get("server").cloned()
}
pub async fn load_config() -> color_eyre::Result<ClientConfig> {
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();
}
+33 -102
View File
@@ -1,19 +1,11 @@
use android_permissions::{PermissionManager, RECORD_AUDIO};
use crate::app::Command; use crate::app::Command;
use color_eyre::eyre::Error; use color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver; use dioxus::hooks::UnboundedReceiver;
use jni::{objects::JObject, JavaVM};
use mumble_web2_common::{ClientConfig, ServerStatus}; use mumble_web2_common::{ClientConfig, ServerStatus};
use std::future::Future; use std::future::Future;
use std::time::Duration; use std::time::Duration;
use super::{PlatformInterface, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait};
pub use super::connect::*; pub use super::connect::*;
pub use super::native_audio::*;
pub use tokio::task::spawn;
pub use tokio::time::sleep;
// ============================================================================ // ============================================================================
// Platform Struct // Platform Struct
@@ -22,67 +14,33 @@ pub use tokio::time::sleep;
/// Mobile platform implementation using Tokio, native audio, and Android permissions. /// Mobile platform implementation using Tokio, native audio, and Android permissions.
pub struct MobilePlatform; pub struct MobilePlatform;
// ============================================================================ impl super::PlatformInterface for MobilePlatform {
// SpawnHandle type AudioSystem = super::native_audio::NativeAudioSystem;
// ============================================================================
pub type SpawnHandle = tokio::runtime::Handle;
impl SpawnHandleTrait for SpawnHandle {
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + 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<F>(future: F)
where
F: Future<Output = ()> + 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<ClientConfig> { async fn load_config() -> color_eyre::Result<ClientConfig> {
load_config().await Ok(ClientConfig {
proxy_url: None,
cert_hash: None,
any_server: true,
})
} }
fn load_username() -> Option<String> { fn load_username() -> Option<String> {
load_username() None
} }
fn load_server_url() -> Option<String> { fn load_server_url() -> Option<String> {
load_server_url() None
} }
fn set_default_username(username: &str) -> Option<()> { fn set_default_username(_username: &str) -> Option<()> {
set_default_username(username) None
} }
fn set_default_server(server: &str) -> Option<()> { fn set_default_server(server: &str) -> Option<()> {
set_default_server(server) None
} }
}
impl PlatformNetwork for MobilePlatform {
async fn network_connect( async fn network_connect(
address: String, address: String,
username: String, username: String,
@@ -95,66 +53,39 @@ impl PlatformNetwork for MobilePlatform {
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> { async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
get_status(client).await get_status(client).await
} }
}
impl PlatformInit for MobilePlatform {
fn init_logging() { 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() { fn request_permissions() {
request_recording_permission(); request_recording_permission();
} }
async fn sleep(duration: Duration) {
tokio::time::sleep(duration).await;
}
} }
impl PlatformInterface for MobilePlatform {} #[cfg(not(target_os = "android"))]
pub fn request_recording_permission() {}
pub fn set_default_username(_username: &str) -> Option<()> {
None
}
pub fn set_default_server(server: &str) -> Option<()> {
None
}
pub fn load_username() -> Option<String> {
None
}
pub fn load_server_url() -> Option<String> {
None
}
pub async fn load_config() -> color_eyre::Result<ClientConfig> {
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(target_os = "android")] #[cfg(target_os = "android")]
pub fn request_recording_permission() { pub fn request_recording_permission() {
use android_permissions::{PermissionManager, RECORD_AUDIO};
use jni::{objects::JObject, JavaVM};
let ctx = ndk_context::android_context(); let ctx = ndk_context::android_context();
let vm = unsafe { JavaVM::from_raw(ctx.vm().cast()).unwrap() }; let vm = unsafe { JavaVM::from_raw(ctx.vm().cast()).unwrap() };
let activity = unsafe { JObject::from_raw(ctx.context().cast()) }; let activity = unsafe { JObject::from_raw(ctx.context().cast()) };
+12 -21
View File
@@ -14,17 +14,6 @@ use std::time::Duration;
// Trait Definitions // 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<F>(&self, future: F)
where
F: Future<Output = ()> + 'static;
/// Get a spawn handle for the current context.
fn current() -> Self;
}
pub trait AudioSystemInterface { pub trait AudioSystemInterface {
type AudioPlayer: AudioPlayerInterface; type AudioPlayer: AudioPlayerInterface;
} }
@@ -36,7 +25,6 @@ pub trait AudioPlayerInterface {}
/// verification that all platforms implement the required functionality. /// verification that all platforms implement the required functionality.
pub trait PlatformInterface { pub trait PlatformInterface {
type AudioSystem: AudioSystemInterface; type AudioSystem: AudioSystemInterface;
type SpawnHandle: SpawnHandleInterface;
/// Initialize logging for the platform. /// Initialize logging for the platform.
fn init_logging(); fn init_logging();
@@ -72,11 +60,6 @@ pub trait PlatformInterface {
/// Save the default server URL. /// Save the default server URL.
fn set_default_server(server: &str) -> Option<()>; fn set_default_server(server: &str) -> Option<()>;
/// Spawn an async task.
fn spawn<F>(future: F)
where
F: Future<Output = ()> + 'static;
/// Async sleep for the given duration. /// Async sleep for the given duration.
fn sleep(duration: Duration) -> impl Future<Output = ()>; fn sleep(duration: Duration) -> impl Future<Output = ()>;
} }
@@ -86,7 +69,7 @@ pub trait PlatformInterface {
// ============================================================================ // ============================================================================
#[cfg(feature = "web")] #[cfg(feature = "web")]
mod web; pub mod web;
#[cfg(any(feature = "desktop", feature = "mobile"))] #[cfg(any(feature = "desktop", feature = "mobile"))]
mod connect; mod connect;
@@ -94,9 +77,9 @@ mod connect;
mod native_audio; mod native_audio;
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
mod desktop; pub mod desktop;
#[cfg(feature = "mobile")] #[cfg(feature = "mobile")]
mod mobile; pub mod mobile;
// ============================================================================ // ============================================================================
// Platform Type Alias // Platform Type Alias
@@ -113,7 +96,15 @@ pub type Platform = mobile::MobilePlatform;
pub type AudioSystem = <Platform as PlatformInterface>::AudioSystem; pub type AudioSystem = <Platform as PlatformInterface>::AudioSystem;
pub type AudioPlayer = <AudioSystem as AudioSystemInterface>::AudioPlayer; pub type AudioPlayer = <AudioSystem as AudioSystemInterface>::AudioPlayer;
pub type SpawnHandle = <Platform as PlatformInterface>::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. /// Compile-time assertion that CurrentPlatform implements Platform.
const _: () = { const _: () = {
+21 -12
View File
@@ -1,19 +1,22 @@
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState}; use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
use color_eyre::eyre::{eyre, Error}; use color_eyre::eyre::{eyre, Error};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _};
use futures::io::{AsyncRead, AsyncWrite};
use std::mem::replace; use std::mem::replace;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
pub trait ImpRead: AsyncRead + Unpin + Send + 'static {} // =============
impl<T: AsyncRead + Unpin + Send + 'static> ImpRead for T {} // Async runtime
// =============
pub use tokio::spawn;
pub type SpawnHandle = tokio::runtime::Handle;
pub trait ImpWrite: AsyncWrite + Unpin + Send + 'static {} // ============
impl<T: AsyncWrite + Unpin + Send + 'static> ImpWrite for T {} // Audio System
// ============
pub struct AudioSystem { pub struct NativeAudioSystem {
output: cpal::Device, output: cpal::Device,
input: cpal::Device, input: cpal::Device,
processors: AudioProcessorSender, processors: AudioProcessorSender,
@@ -52,13 +55,13 @@ fn encode_and_send(
type Buffer = Arc<Mutex<dasp_ring_buffer::Bounded<Vec<i16>>>>; type Buffer = Arc<Mutex<dasp_ring_buffer::Bounded<Vec<i16>>>>;
impl AudioSystem { impl NativeAudioSystem {
pub async fn new() -> Result<Self, Error> { pub async fn new() -> Result<Self, Error> {
// TODO // TODO
let host = cpal::default_host(); let host = cpal::default_host();
let name = host.id(); let name = host.id();
let processors = AudioProcessorSender::default(); let processors = AudioProcessorSender::default();
Ok(AudioSystem { Ok(NativeAudioSystem {
output: host output: host
.default_output_device() .default_output_device()
.ok_or(eyre!("no output devices from {name:?}"))?, .ok_or(eyre!("no output devices from {name:?}"))?,
@@ -145,7 +148,7 @@ impl AudioSystem {
} }
} }
pub fn create_player(&mut self) -> Result<AudioPlayer, Error> { pub fn create_player(&mut self) -> Result<NativeAudioPlayer, Error> {
let config = self.choose_config(self.output.supported_output_configs()?)?; let config = self.choose_config(self.output.supported_output_configs()?)?;
info!( info!(
"creating player on {:?} with {:#?}", "creating player on {:?} with {:#?}",
@@ -183,7 +186,7 @@ impl AudioSystem {
)? )?
}; };
stream.play()?; stream.play()?;
Ok(AudioPlayer { Ok(NativeAudioPlayer {
decoder, decoder,
stream, stream,
buffer, buffer,
@@ -192,14 +195,18 @@ impl AudioSystem {
} }
} }
pub struct AudioPlayer { impl super::AudioSystemInterface for NativeAudioSystem {
type AudioPlayer = NativeAudioPlayer;
}
pub struct NativeAudioPlayer {
decoder: opus::Decoder, decoder: opus::Decoder,
stream: cpal::Stream, stream: cpal::Stream,
buffer: Buffer, buffer: Buffer,
tmp: Vec<i16>, tmp: Vec<i16>,
} }
impl AudioPlayer { impl NativeAudioPlayer {
pub fn play_opus(&mut self, payload: &[u8]) { pub fn play_opus(&mut self, payload: &[u8]) {
let len = match self.decoder.decode(payload, &mut self.tmp, false) { let len = match self.decoder.decode(payload, &mut self.tmp, false) {
Ok(l) => l, Ok(l) => l,
@@ -221,3 +228,5 @@ impl AudioPlayer {
} }
} }
} }
impl super::AudioPlayerInterface for NativeAudioPlayer {}
+22 -30
View File
@@ -38,14 +38,34 @@ use web_sys::WorkletOptions;
use web_sys::{console, window}; use web_sys::{console, window};
use web_sys::{AudioContext, AudioDataCopyToOptions}; use web_sys::{AudioContext, AudioDataCopyToOptions};
pub use wasm_bindgen_futures::spawn_local as spawn;
pub trait ImpRead: AsyncRead + Unpin + 'static {} pub trait ImpRead: AsyncRead + Unpin + 'static {}
impl<T: AsyncRead + Unpin + 'static> ImpRead for T {} impl<T: AsyncRead + Unpin + 'static> ImpRead for T {}
pub trait ImpWrite: AsyncWrite + Unpin + 'static {} pub trait ImpWrite: AsyncWrite + Unpin + 'static {}
impl<T: AsyncWrite + Unpin + 'static> ImpWrite for T {} impl<T: AsyncWrite + Unpin + 'static> ImpWrite for T {}
// =============
// Async runtime
// =============
pub use wasm_bindgen_futures::spawn_local as spawn;
#[derive(Clone)]
pub struct SpawnHandle;
impl SpawnHandle {
pub fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
pub fn current() -> Self {
SpawnHandle
}
}
// ============================================================================ // ============================================================================
// Platform Struct // Platform Struct
// ============================================================================ // ============================================================================
@@ -53,29 +73,8 @@ impl<T: AsyncWrite + Unpin + 'static> ImpWrite for T {}
/// Web platform implementation using WebTransport and Web Audio API. /// Web platform implementation using WebTransport and Web Audio API.
pub struct WebPlatform; pub struct WebPlatform;
// ============================================================================
// Trait Implementations
// ============================================================================
#[derive(Clone)]
pub struct WebSpawnHandle;
impl super::SpawnHandleInterface for WebSpawnHandle {
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
fn current() -> Self {
WebSpawnHandle
}
}
impl super::PlatformInterface for WebPlatform { impl super::PlatformInterface for WebPlatform {
type AudioSystem = WebAudioSystem; type AudioSystem = WebAudioSystem;
type SpawnHandle = WebSpawnHandle;
fn init_logging() { fn init_logging() {
init_logging(); init_logging();
@@ -118,13 +117,6 @@ impl super::PlatformInterface for WebPlatform {
get_status(client).await get_status(client).await
} }
fn spawn<F>(future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
async fn sleep(duration: Duration) { async fn sleep(duration: Duration) {
TimeoutFuture::new(duration.as_millis() as u32).await; TimeoutFuture::new(duration.as_millis() as u32).await;
} }