Refactor the imp/gui bondary to use real traits (#18)
Build Mumble Web 2 / linux_build (push) Successful in 1m24s
Build Mumble Web 2 / windows_build (push) Successful in 2m36s
Build Mumble Web 2 / android_build (push) Successful in 5m57s
Build android container / android-release-builder-container-build (push) Successful in -4s
Build Mumble Web 2 release builder containers / windows-release-builder-container-build (push) Successful in 16s
Build Mumble Web 2 / linux_build (push) Successful in 1m24s
Build Mumble Web 2 / windows_build (push) Successful in 2m36s
Build Mumble Web 2 / android_build (push) Successful in 5m57s
Build android container / android-release-builder-container-build (push) Successful in -4s
Build Mumble Web 2 release builder containers / windows-release-builder-container-build (push) Successful in 16s
# Summary Introduces a trait-based platform abstraction layer that makes the boundary between platform-specific and shared code explicit and compile-time verified. The TLDR version of this new trait stuff works: 1. Define a `PlatformInterface` trait. 2. Each platform defines a zero-sized struct implementing the trait (ex `WebPlatform`). 3. Create an ifdef'd type alias on those structs: ```rust #[cfg(feature = "web")] pub type Platform = web::WebPlatform; #[cfg(all(feature = "desktop"))] pub type Platform = desktop::DesktopPlatform; #[cfg(all(feature = "mobile", not(feature = "web")))] pub type Platform = mobile::MobilePlatform; ``` 5. Add a compile time assertion that `Platform` implements `PlatformInterface`. # Motivation Previously, platform code used a mix of pub use re-exports and #[cfg] blocks that made it difficult to understand what each platform must implement. The new trait-based approach provides: - Clear documentation of the platform contract - Compile-time verification that all platforms implement required functionality - Ability to cargo check without feature flags (via stub platform) # Changes New traits in imp/mod.rs: - PlatformInterface - logging, permissions, network, config, storage. Overall this the trait that platforms must satify to compile. - AudioSystemInterface - audio system initialization and recording - AudioPlayerInterface - opus audio playback Type aliases: - Platform, AudioSystem, AudioPlayer resolve to the correct types based on feature flags Call site updates: - Changed from imp::function() to Platform::function() syntax - Removed ImpRead/ImpWrite helper traits in favor of direct bounds # Testing Manual testing reveals that Web and Desktop still work, I (Liam) have not tested the mobile version beyond compilation. Co-authored-by: Liam Warfield <liam.warfield@gmail.com> Reviewed-on: #18 Co-authored-by: Sam Sartor <me@samsartor.com> Co-committed-by: Sam Sartor <me@samsartor.com>
This commit was merged in pull request #18.
This commit is contained in:
+122
-108
@@ -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;
|
||||
@@ -29,7 +28,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,16 +37,119 @@ use web_sys::WorkletOptions;
|
||||
use web_sys::{console, window};
|
||||
use web_sys::{AudioContext, AudioDataCopyToOptions};
|
||||
|
||||
#[allow(unused)]
|
||||
pub use wasm_bindgen_futures::spawn_local as spawn;
|
||||
|
||||
pub trait ImpRead: AsyncRead + Unpin + 'static {}
|
||||
impl<T: AsyncRead + Unpin + 'static> ImpRead for T {}
|
||||
#[allow(unused)]
|
||||
#[derive(Clone)]
|
||||
pub struct SpawnHandle;
|
||||
|
||||
pub trait ImpWrite: AsyncWrite + Unpin + 'static {}
|
||||
impl<T: AsyncWrite + Unpin + 'static> ImpWrite for T {}
|
||||
impl SpawnHandle {
|
||||
pub fn spawn<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
wasm_bindgen_futures::spawn_local(future);
|
||||
}
|
||||
|
||||
pub async fn sleep(d: Duration) {
|
||||
TimeoutFuture::new(d.as_millis() as u32).await
|
||||
pub fn current() -> Self {
|
||||
SpawnHandle
|
||||
}
|
||||
}
|
||||
|
||||
/// Web platform implementation using WebTransport and Web Audio API.
|
||||
pub struct WebPlatform;
|
||||
|
||||
impl super::PlatformInterface for WebPlatform {
|
||||
type AudioSystem = WebAudioSystem;
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
fn request_permissions() {
|
||||
// No-op on web
|
||||
}
|
||||
|
||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||
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::<ClientConfig>()
|
||||
.await?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn load_username() -> Option<String> {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.ok()??
|
||||
.get_item("username")
|
||||
.ok()?
|
||||
}
|
||||
|
||||
fn load_server_url() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_username(username: &str) -> Option<()> {
|
||||
web_sys::window()?
|
||||
.local_storage()
|
||||
.ok()??
|
||||
.set_item("username", username)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn set_default_server(_server: &str) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
network_connect(address, username, event_rx, gui_config).await
|
||||
}
|
||||
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
Ok(client
|
||||
.get(absolute_url("status")?)
|
||||
.send()
|
||||
.await?
|
||||
.json::<ServerStatus>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn sleep(duration: Duration) {
|
||||
TimeoutFuture::new(duration.as_millis() as u32).await;
|
||||
}
|
||||
}
|
||||
|
||||
trait ResultExt<T> {
|
||||
@@ -73,7 +174,7 @@ impl<T> ResultExt<T> for Result<T, JsError> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioSystem {
|
||||
pub struct WebAudioSystem {
|
||||
webctx: AudioContext,
|
||||
processors: AudioProcessorSender,
|
||||
}
|
||||
@@ -104,8 +205,10 @@ async fn attach_worklet(audio_context: &AudioContext) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl AudioSystem {
|
||||
pub async fn new() -> Result<Self, Error> {
|
||||
impl super::AudioSystemInterface for WebAudioSystem {
|
||||
type AudioPlayer = WebAudioPlayer;
|
||||
|
||||
async fn new() -> Result<Self, Error> {
|
||||
// Create MediaStreams to playback decoded audio
|
||||
// The audio context is used to reproduce audio.
|
||||
let webctx = configure_audio_context();
|
||||
@@ -113,17 +216,14 @@ impl AudioSystem {
|
||||
|
||||
let processors = AudioProcessorSender::default();
|
||||
|
||||
Ok(AudioSystem { webctx, processors })
|
||||
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<u8>, bool) + 'static,
|
||||
) -> Result<(), Error> {
|
||||
fn start_recording(&mut self, each: impl FnMut(Vec<u8>, bool) + 'static) -> Result<(), Error> {
|
||||
let audio_context_worklet = self.webctx.clone();
|
||||
let processors = self.processors.clone();
|
||||
spawn(async move {
|
||||
@@ -135,7 +235,7 @@ impl AudioSystem {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_player(&mut self) -> Result<AudioPlayer, Error> {
|
||||
fn create_player(&mut self) -> Result<WebAudioPlayer, Error> {
|
||||
let sink_node = AudioWorkletNode::new(&self.webctx, "rust_speaker_worklet").ey()?;
|
||||
|
||||
// Connect worklet to destination
|
||||
@@ -188,14 +288,14 @@ 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 {
|
||||
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(
|
||||
@@ -418,94 +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<String> {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.ok()??
|
||||
.get_item("username")
|
||||
.ok()?
|
||||
}
|
||||
|
||||
pub fn load_server_url() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
||||
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<ClientConfig> {
|
||||
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::<ClientConfig>()
|
||||
.await?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
Ok(client
|
||||
.get(absolute_url("status")?)
|
||||
.send()
|
||||
.await?
|
||||
.json::<ServerStatus>()
|
||||
.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");
|
||||
}
|
||||
|
||||
pub struct SpawnHandle;
|
||||
|
||||
impl SpawnHandle {
|
||||
pub fn current() -> Self {
|
||||
SpawnHandle
|
||||
}
|
||||
|
||||
pub fn spawn<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
spawn(future);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user