Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26a08acc36 | |||
| b20ed1ff56 | |||
| 765446392d | |||
| 2c22942fb3 | |||
| 75990ca9ce | |||
| 9f6557bb92 |
@@ -1 +0,0 @@
|
|||||||
target
|
|
||||||
@@ -42,47 +42,6 @@ jobs:
|
|||||||
path: target/release/mumble-web2-proxy
|
path: target/release/mumble-web2-proxy
|
||||||
retention-days: 5
|
retention-days: 5
|
||||||
|
|
||||||
macos_build:
|
|
||||||
runs-on: macos
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Restore Rust cache
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo
|
|
||||||
./target
|
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
rust-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Install cargo binstall
|
|
||||||
run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
|
||||||
|
|
||||||
- name: Install dioxus-cli
|
|
||||||
run: cargo binstall dioxus-cli --version 0.7.3 --no-confirm
|
|
||||||
|
|
||||||
- name: Build dioxus project
|
|
||||||
run: dx bundle --platform macos --release -p mumble-web2-gui
|
|
||||||
|
|
||||||
- name: Save Rust cache
|
|
||||||
if: always()
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo
|
|
||||||
./target
|
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Upload mumble-web2-gui Artifact
|
|
||||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: mumble-web2-gui-macos-arm64
|
|
||||||
path: gui/dist
|
|
||||||
retention-days: 5
|
|
||||||
|
|
||||||
windows_build:
|
windows_build:
|
||||||
runs-on: windows
|
runs-on: windows
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
+1
-1
@@ -6,4 +6,4 @@ server_hash.txt
|
|||||||
proxy/bundle
|
proxy/bundle
|
||||||
/config.toml
|
/config.toml
|
||||||
proxy/config.toml
|
proxy/config.toml
|
||||||
*_onnx.tar.gz
|
gui/assets/*_onnx.tar.gz
|
||||||
|
|||||||
Generated
+14
-26
@@ -1587,9 +1587,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dioxus-core-types"
|
name = "dioxus-core-types"
|
||||||
version = "0.7.4"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b99d7d199aad72431b549759550002e7d72c8a257eba500dca9fbdb2122de103"
|
checksum = "bfc4b8cdc440a55c17355542fc2089d97949bba674255d84cac77805e1db8c9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dioxus-desktop"
|
name = "dioxus-desktop"
|
||||||
@@ -4220,7 +4220,14 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mumble-web2-client"
|
name = "mumble-web2-common"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mumble-web2-gui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-permissions",
|
"android-permissions",
|
||||||
@@ -4234,8 +4241,9 @@ dependencies = [
|
|||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"dasp_ring_buffer",
|
"dasp_ring_buffer",
|
||||||
"deep_filter",
|
"deep_filter",
|
||||||
|
"dioxus",
|
||||||
"dioxus-asset-resolver",
|
"dioxus-asset-resolver",
|
||||||
"dioxus-signals",
|
"dioxus-web",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@@ -4244,7 +4252,6 @@ dependencies = [
|
|||||||
"jni",
|
"jni",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"lol_html 2.7.0",
|
"lol_html 2.7.0",
|
||||||
"manganis",
|
|
||||||
"markdown",
|
"markdown",
|
||||||
"merge-io",
|
"merge-io",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
@@ -4256,6 +4263,7 @@ dependencies = [
|
|||||||
"opus",
|
"opus",
|
||||||
"ordermap",
|
"ordermap",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rfd 0.16.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-wasm-bindgen",
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4271,27 +4279,6 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mumble-web2-common"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"color-eyre",
|
|
||||||
"serde",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mumble-web2-gui"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"color-eyre",
|
|
||||||
"dioxus",
|
|
||||||
"dioxus-web",
|
|
||||||
"mumble-web2-client",
|
|
||||||
"mumble-web2-common",
|
|
||||||
"rfd 0.16.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mumble-web2-proxy"
|
name = "mumble-web2-proxy"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -4300,6 +4287,7 @@ dependencies = [
|
|||||||
"hmac-sha256",
|
"hmac-sha256",
|
||||||
"mumble-web2-common",
|
"mumble-web2-common",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"rand 0.9.2",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"rustls",
|
"rustls",
|
||||||
"salvo",
|
"salvo",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["client", "common", "gui", "proxy"]
|
members = ["common", "gui", "proxy"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = { version = "1.0.214", features = ["derive"] }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "mumble-web2-client"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# Web Dependencies
|
|
||||||
# ================
|
|
||||||
wasm-bindgen = { version = "^0.2.92", optional = true }
|
|
||||||
wasm-bindgen-futures = { version = "^0.4.42", optional = true }
|
|
||||||
wasm-streams = { version = "^0.4.0", optional = true }
|
|
||||||
serde-wasm-bindgen = { version = "^0.6.5", optional = true }
|
|
||||||
js-sys = { version = "=0.3.82", optional = true }
|
|
||||||
web-sys = { version = "=0.3.82", features = [
|
|
||||||
"WebTransport",
|
|
||||||
"console",
|
|
||||||
"WebTransportOptions",
|
|
||||||
"WebTransportBidirectionalStream",
|
|
||||||
"WebTransportSendStream",
|
|
||||||
"WebTransportReceiveStream",
|
|
||||||
"Navigator",
|
|
||||||
"MediaDevices",
|
|
||||||
"AudioDecoder",
|
|
||||||
"AudioDecoderInit",
|
|
||||||
"AudioData",
|
|
||||||
"AudioEncoderConfig",
|
|
||||||
"AudioDecoderConfig",
|
|
||||||
"EncodedAudioChunk",
|
|
||||||
"EncodedAudioChunkInit",
|
|
||||||
"EncodedAudioChunkType",
|
|
||||||
"CodecState",
|
|
||||||
"AudioContext",
|
|
||||||
"AudioContextOptions",
|
|
||||||
"MediaStream",
|
|
||||||
"GainNode",
|
|
||||||
"MediaStreamAudioSourceNode",
|
|
||||||
"BaseAudioContext",
|
|
||||||
"AudioDestinationNode",
|
|
||||||
"AudioWorkletNode",
|
|
||||||
"AudioWorklet",
|
|
||||||
"AudioWorkletProcessor",
|
|
||||||
"MessagePort",
|
|
||||||
"MediaStreamConstraints",
|
|
||||||
"WorkletOptions",
|
|
||||||
"AudioEncoder",
|
|
||||||
"AudioEncoderInit",
|
|
||||||
"AudioDataInit",
|
|
||||||
"HtmlAnchorElement",
|
|
||||||
"Url",
|
|
||||||
"Blob",
|
|
||||||
"AudioDataCopyToOptions",
|
|
||||||
"AudioSampleFormat",
|
|
||||||
"Storage",
|
|
||||||
], optional = true }
|
|
||||||
gloo-timers = { version = "^0.3.0", features = ["futures"], optional = true }
|
|
||||||
tracing-web = { version = "^0.1.3", optional = true }
|
|
||||||
|
|
||||||
# Desktop Dependecies
|
|
||||||
# ===================
|
|
||||||
tokio = { version = "^1.41.1", features = ["net", "rt"], optional = true }
|
|
||||||
tokio-rustls = { version = "^0.26.0", optional = true }
|
|
||||||
opus = { version = "0.3.0", optional = true }
|
|
||||||
cpal = { version = "0.15.3", optional = true }
|
|
||||||
dasp_ring_buffer = { version = "0.11.0", optional = true }
|
|
||||||
etcetera = { version = "0.10.0", optional = true }
|
|
||||||
|
|
||||||
# Base Dependencies
|
|
||||||
# ================
|
|
||||||
dioxus-signals = "0.7.2"
|
|
||||||
manganis = "0.7.2"
|
|
||||||
once_cell = "1.19.0"
|
|
||||||
asynchronous-codec = { workspace = true }
|
|
||||||
futures = "^0.3.30"
|
|
||||||
merge-io = "^0.3.0"
|
|
||||||
mumble-protocol = { workspace = true }
|
|
||||||
serde_json = "1"
|
|
||||||
tokio-util = { version = "^0.7.11", features = ["codec", "compat"] }
|
|
||||||
byteorder = "1.5"
|
|
||||||
ogg = "^0.9.1"
|
|
||||||
ordermap = "^0.5.3"
|
|
||||||
html-purifier = "^0.3.0"
|
|
||||||
markdown = "^0.3.0"
|
|
||||||
futures-channel = "^0.3.30"
|
|
||||||
mumble-web2-common = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
tracing-subscriber = { version = "^0.3.18", features = ["ansi"] }
|
|
||||||
tracing = "^0.1.40"
|
|
||||||
color-eyre = "^0.6.3"
|
|
||||||
crossbeam-queue = "^0.3.11"
|
|
||||||
lol_html = "^2.2.0"
|
|
||||||
base64 = "^0.22"
|
|
||||||
mime_guess = "^2.0.5"
|
|
||||||
async_cell = "^0.2.3"
|
|
||||||
reqwest = { version = "^0.12.22", features = ["json"] }
|
|
||||||
dioxus-asset-resolver = "0.7.2"
|
|
||||||
|
|
||||||
# Denoising
|
|
||||||
# =========
|
|
||||||
deep_filter = { git = "https://github.com/Rikorose/DeepFilterNet.git", rev = "d375b2d8309e0935d165700c91da9de862a99c31", features = [
|
|
||||||
"tract",
|
|
||||||
] }
|
|
||||||
crossbeam = "0.8.4"
|
|
||||||
|
|
||||||
# Android dependencies for requesting permissions
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
|
||||||
android-permissions = "0.1.2"
|
|
||||||
jni = "0.21.1"
|
|
||||||
ndk-context = "0.1.1"
|
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
tract-hir = "=0.12.4"
|
|
||||||
tract-core = "=0.12.4"
|
|
||||||
tract-onnx = "=0.12.4"
|
|
||||||
tract-pulse = "=0.12.4"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
web = [
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"wasm-streams",
|
|
||||||
"serde-wasm-bindgen",
|
|
||||||
"js-sys",
|
|
||||||
"web-sys",
|
|
||||||
"gloo-timers",
|
|
||||||
"tracing-web",
|
|
||||||
"deep_filter/wasm",
|
|
||||||
]
|
|
||||||
desktop = [
|
|
||||||
"tokio",
|
|
||||||
"tokio-rustls",
|
|
||||||
"tracing-subscriber/env-filter",
|
|
||||||
"opus",
|
|
||||||
"cpal",
|
|
||||||
"dasp_ring_buffer",
|
|
||||||
"etcetera",
|
|
||||||
"mumble-web2-common/networking",
|
|
||||||
]
|
|
||||||
mobile = [
|
|
||||||
"tokio",
|
|
||||||
"tokio-rustls",
|
|
||||||
"tracing-subscriber/env-filter",
|
|
||||||
"opus",
|
|
||||||
"cpal",
|
|
||||||
"dasp_ring_buffer",
|
|
||||||
"mumble-web2-common/networking",
|
|
||||||
]
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
use dioxus_signals::{ReadableExt as _, Signal};
|
|
||||||
use mime_guess::Mime;
|
|
||||||
use mumble_web2_common::ProxyOverrides;
|
|
||||||
use ordermap::OrderSet;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::{fmt, sync::Arc};
|
|
||||||
|
|
||||||
pub type ChannelId = u32;
|
|
||||||
pub type UserId = u32;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ConnectionState {
|
|
||||||
Disconnected,
|
|
||||||
Connecting,
|
|
||||||
Connected,
|
|
||||||
Failed(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AudioSettings {
|
|
||||||
pub denoise: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ConnectTarget {
|
|
||||||
Direct { host: String, port: u16 },
|
|
||||||
Proxy(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Command {
|
|
||||||
Connect {
|
|
||||||
target: ConnectTarget,
|
|
||||||
username: String,
|
|
||||||
config: ProxyOverrides,
|
|
||||||
},
|
|
||||||
SendChat {
|
|
||||||
markdown: String,
|
|
||||||
channels: Vec<ChannelId>,
|
|
||||||
},
|
|
||||||
SendFile {
|
|
||||||
bytes: Vec<u8>,
|
|
||||||
name: String,
|
|
||||||
mime: Option<Mime>,
|
|
||||||
channels: Vec<ChannelId>,
|
|
||||||
},
|
|
||||||
SetMute {
|
|
||||||
mute: bool,
|
|
||||||
},
|
|
||||||
SetDeaf {
|
|
||||||
deaf: bool,
|
|
||||||
},
|
|
||||||
EnterChannel {
|
|
||||||
channel: ChannelId,
|
|
||||||
user: UserId,
|
|
||||||
},
|
|
||||||
UpdateAudioSettings(AudioSettings),
|
|
||||||
Disconnect,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct UserState {
|
|
||||||
pub name: String,
|
|
||||||
pub channel: ChannelId,
|
|
||||||
pub deaf: bool,
|
|
||||||
pub mute: bool,
|
|
||||||
pub suppress: bool,
|
|
||||||
pub self_deaf: bool,
|
|
||||||
pub self_mute: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Chat {
|
|
||||||
pub raw: String,
|
|
||||||
pub dangerous_html: String,
|
|
||||||
pub sender: Option<UserId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct ChannelState {
|
|
||||||
pub name: String,
|
|
||||||
pub children: OrderSet<ChannelId>,
|
|
||||||
pub users: OrderSet<UserId>,
|
|
||||||
pub parent: Option<ChannelId>,
|
|
||||||
pub position: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChannelState {
|
|
||||||
pub fn update_from_channel_state(
|
|
||||||
&mut self,
|
|
||||||
channel_state: &mumble_protocol::control::msgs::ChannelState,
|
|
||||||
) {
|
|
||||||
if channel_state.has_position() {
|
|
||||||
self.position = channel_state.get_position();
|
|
||||||
}
|
|
||||||
if channel_state.has_parent() {
|
|
||||||
self.parent = Some(channel_state.get_parent());
|
|
||||||
}
|
|
||||||
if channel_state.has_name() {
|
|
||||||
self.name = channel_state.get_name().to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct ChannelsState {
|
|
||||||
pub channels: HashMap<ChannelId, ChannelState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChannelsState {
|
|
||||||
pub fn update_from_channel_state(
|
|
||||||
&mut self,
|
|
||||||
channel_state: &mumble_protocol::control::msgs::ChannelState,
|
|
||||||
) {
|
|
||||||
self.channels
|
|
||||||
.entry(channel_state.get_channel_id())
|
|
||||||
.or_default()
|
|
||||||
.update_from_channel_state(channel_state);
|
|
||||||
|
|
||||||
self.update_channel_parents();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_from_channel_remove(
|
|
||||||
&mut self,
|
|
||||||
channel_remove: &mumble_protocol::control::msgs::ChannelRemove,
|
|
||||||
) {
|
|
||||||
self.channels.remove(&channel_remove.get_channel_id());
|
|
||||||
self.update_channel_parents();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_channel_parents(&mut self) {
|
|
||||||
// Zero out existing children
|
|
||||||
for state in self.channels.values_mut() {
|
|
||||||
state.children.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut to_sort: Vec<(ChannelId, Option<ChannelId>, i32, String)> = Vec::new();
|
|
||||||
for (id, state) in self.channels.iter() {
|
|
||||||
// Handle channels with no parent (the root channel)
|
|
||||||
let Some(parent_id) = state.parent else {
|
|
||||||
to_sort.push((*id, None, 0, state.name.clone()));
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// If a channel has a parent that we haven't gotten a channel
|
|
||||||
// state packet for, ignore it
|
|
||||||
if !self.channels.contains_key(&parent_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
to_sort.push((*id, Some(parent_id), state.position, state.name.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let pos_name: HashMap<ChannelId, (i32, String)> = self
|
|
||||||
.channels
|
|
||||||
.iter()
|
|
||||||
.map(|(&id, state)| (id, (state.position, state.name.clone())))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut updated: HashSet<ChannelId> = HashSet::new();
|
|
||||||
|
|
||||||
while updated.len() < to_sort.len() {
|
|
||||||
for &(id, ref parent_id, position, ref name) in &to_sort {
|
|
||||||
let Some(parent_id) = parent_id else {
|
|
||||||
updated.insert(id);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if updated.contains(&id) || !updated.contains(&parent_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap should never fail here since we pre filter
|
|
||||||
let parent = self.channels.get_mut(&parent_id).unwrap();
|
|
||||||
|
|
||||||
let mut insert_index = parent.children.len();
|
|
||||||
for (i, &child) in parent.children.iter().enumerate() {
|
|
||||||
let (p, ref n) = pos_name[&child];
|
|
||||||
if (position == p && name < n) || p > position {
|
|
||||||
insert_index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.children.insert_before(insert_index, id);
|
|
||||||
updated.insert(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct ServerState {
|
|
||||||
pub channels_state: ChannelsState,
|
|
||||||
pub users: HashMap<UserId, UserState>,
|
|
||||||
pub chat: Vec<Chat>,
|
|
||||||
pub session: Option<UserId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerState {
|
|
||||||
pub fn this_user(&self) -> Option<&UserState> {
|
|
||||||
self.users.get(&self.session?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct State {
|
|
||||||
pub status: Signal<ConnectionState>,
|
|
||||||
pub server: Signal<ServerState>,
|
|
||||||
pub audio: Signal<AudioSettings>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for State {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("State")
|
|
||||||
.field("status", &self.status.read())
|
|
||||||
.field("server", &self.server.read())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SharedState = Arc<State>;
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
use crate::app::{Command, ConnectTarget, SharedState};
|
|
||||||
use color_eyre::eyre::Error;
|
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
/// Desktop platform implementation using Tokio and native audio.
|
|
||||||
pub struct DesktopPlatform;
|
|
||||||
|
|
||||||
impl super::PlatformInterface for DesktopPlatform {
|
|
||||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
|
||||||
type ConfigSystem = super::native_config::NativeConfigSystem;
|
|
||||||
|
|
||||||
async fn sleep(duration: Duration) {
|
|
||||||
tokio::time::sleep(duration).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
|
||||||
Ok(ProxyOverrides {
|
|
||||||
proxy_url: None,
|
|
||||||
cert_hash: None,
|
|
||||||
any_server: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn network_connect(
|
|
||||||
target: ConnectTarget,
|
|
||||||
username: String,
|
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
|
||||||
overrides: &ProxyOverrides,
|
|
||||||
state: SharedState,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
super::connect::network_connect(target, username, event_rx, overrides, state).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_status(
|
|
||||||
_client: &reqwest::Client,
|
|
||||||
address: &str,
|
|
||||||
) -> color_eyre::Result<ServerStatus> {
|
|
||||||
let (host, port) = match address.rsplit_once(':') {
|
|
||||||
Some((h, p)) => (h, p.parse().unwrap_or(64738)),
|
|
||||||
None => (address, 64738),
|
|
||||||
};
|
|
||||||
mumble_web2_common::ping_server(host, port).await
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_permissions() {
|
|
||||||
// No-op on desktop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
use crate::app::{Command, ConnectTarget, SharedState};
|
|
||||||
use color_eyre::eyre::Error;
|
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
|
||||||
pub struct MobilePlatform;
|
|
||||||
|
|
||||||
impl super::PlatformInterface for MobilePlatform {
|
|
||||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
|
||||||
type ConfigSystem = super::native_config::NativeConfigSystem;
|
|
||||||
|
|
||||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
|
||||||
Ok(ProxyOverrides {
|
|
||||||
proxy_url: None,
|
|
||||||
cert_hash: None,
|
|
||||||
any_server: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn network_connect(
|
|
||||||
target: ConnectTarget,
|
|
||||||
username: String,
|
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
|
||||||
overrides: &ProxyOverrides,
|
|
||||||
state: SharedState,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
super::connect::network_connect(target, username, event_rx, overrides, state).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_status(
|
|
||||||
_client: &reqwest::Client,
|
|
||||||
address: &str,
|
|
||||||
) -> color_eyre::Result<ServerStatus> {
|
|
||||||
let (host, port) = match address.rsplit_once(':') {
|
|
||||||
Some((h, p)) => (h, p.parse().unwrap_or(64738)),
|
|
||||||
None => (address, 64738),
|
|
||||||
};
|
|
||||||
mumble_web2_common::ping_server(host, port).await
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_permissions() {
|
|
||||||
request_recording_permission();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn sleep(duration: Duration) {
|
|
||||||
tokio::time::sleep(duration).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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()) };
|
|
||||||
|
|
||||||
let manager = PermissionManager::create(vm, activity).unwrap();
|
|
||||||
if !manager.check(&RECORD_AUDIO).unwrap() {
|
|
||||||
manager.request(&[&RECORD_AUDIO]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
use color_eyre::eyre::Error;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub struct NativeConfigSystem {
|
|
||||||
config_path: std::path::PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::ConfigSystemInterface for NativeConfigSystem {
|
|
||||||
fn new() -> color_eyre::Result<Self, Error> {
|
|
||||||
return Ok(NativeConfigSystem {
|
|
||||||
config_path: get_config_path()?,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
|
||||||
where
|
|
||||||
T: serde::de::DeserializeOwned,
|
|
||||||
{
|
|
||||||
let config = load_config_map(&self.config_path);
|
|
||||||
|
|
||||||
let Some(value_untyped) = config.get(key).cloned().or_else(|| config_get_default(key))
|
|
||||||
else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
match serde_json::from_value::<T>(value_untyped) {
|
|
||||||
Ok(v) => Some(v),
|
|
||||||
Err(_) => {
|
|
||||||
let default_value = config_get_default(key)?;
|
|
||||||
serde_json::from_value::<T>(default_value).ok()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_set<T>(&self, key: &str, value: &T)
|
|
||||||
where
|
|
||||||
T: serde::Serialize,
|
|
||||||
{
|
|
||||||
let mut config = load_config_map(&self.config_path);
|
|
||||||
let json_value = serde_json::to_value(value).expect("failed to serialize config value");
|
|
||||||
config.insert(key.to_string(), json_value);
|
|
||||||
save_config_map(&config).expect("failed to set config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(feature = "desktop"))]
|
|
||||||
fn get_config_path() -> color_eyre::Result<std::path::PathBuf> {
|
|
||||||
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
|
||||||
|
|
||||||
let strategy = choose_app_strategy(AppStrategyArgs {
|
|
||||||
top_level_domain: "xyz".to_string(),
|
|
||||||
author: "ohea".to_string(),
|
|
||||||
app_name: "Mumble Web2".to_string(),
|
|
||||||
})
|
|
||||||
.expect("failed to choose app strategy");
|
|
||||||
Ok(strategy.config_dir().join("config.json"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
fn get_config_path() -> color_eyre::Result<std::path::PathBuf> {
|
|
||||||
let ctx = ndk_context::android_context();
|
|
||||||
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }?;
|
|
||||||
let mut env = vm.attach_current_thread()?;
|
|
||||||
let ctx = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
|
|
||||||
let cache_dir = env
|
|
||||||
.call_method(ctx, "getFilesDir", "()Ljava/io/File;", &[])?
|
|
||||||
.l()?;
|
|
||||||
let cache_dir: jni::objects::JString = env
|
|
||||||
.call_method(&cache_dir, "toString", "()Ljava/lang/String;", &[])?
|
|
||||||
.l()?
|
|
||||||
.try_into()?;
|
|
||||||
let cache_dir = env.get_string(&cache_dir)?;
|
|
||||||
let cache_dir = cache_dir.to_str()?;
|
|
||||||
Ok(std::path::PathBuf::from(cache_dir).join("config.json"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_config_map(config_path: &std::path::PathBuf) -> HashMap<String, serde_json::Value> {
|
|
||||||
match std::fs::read_to_string(config_path) {
|
|
||||||
Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(),
|
|
||||||
Err(_) => HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_config_map(config: &HashMap<String, serde_json::Value>) -> color_eyre::Result<()> {
|
|
||||||
let config_path = get_config_path().expect("Could not get config file path.");
|
|
||||||
if let Some(parent) = config_path.parent() {
|
|
||||||
info!("Creating config directory: {}", parent.display());
|
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
let contents = serde_json::to_string_pretty(config)?;
|
|
||||||
info!("Writing config to {}", config_path.display());
|
|
||||||
std::fs::write(&config_path, contents)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_get_default(key: &str) -> Option<serde_json::Value> {
|
|
||||||
let default_config = platform_default_config();
|
|
||||||
default_config
|
|
||||||
.get(key)
|
|
||||||
.cloned()
|
|
||||||
.or(super::global_default_config().get(key).cloned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn platform_default_config() -> HashMap<String, serde_json::Value> {
|
|
||||||
serde_json::json!({})
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
mod app;
|
|
||||||
mod effects;
|
|
||||||
mod imp;
|
|
||||||
mod mainloop;
|
|
||||||
mod msghtml;
|
|
||||||
|
|
||||||
pub use app::*;
|
|
||||||
pub use imp::*;
|
|
||||||
pub use mainloop::*;
|
|
||||||
pub use mime_guess;
|
|
||||||
pub use reqwest;
|
|
||||||
|
|
||||||
pub const VERSION: Option<&str> = option_env!("MUMBLE_WEB2_VERSION");
|
|
||||||
@@ -3,10 +3,5 @@ name = "mumble-web2-common"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
|
||||||
networking = ["dep:tokio", "dep:color-eyre"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
tokio = { version = "1", features = ["net", "time"], optional = true }
|
|
||||||
color-eyre = { version = "0.6", optional = true }
|
|
||||||
|
|||||||
+1
-59
@@ -1,7 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
pub struct ProxyOverrides {
|
pub struct ClientConfig {
|
||||||
pub proxy_url: Option<String>,
|
pub proxy_url: Option<String>,
|
||||||
pub cert_hash: Option<Vec<u8>>,
|
pub cert_hash: Option<Vec<u8>>,
|
||||||
pub any_server: bool,
|
pub any_server: bool,
|
||||||
@@ -26,61 +26,3 @@ pub struct ServerEntry {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mumble UDP ping protocol.
|
|
||||||
///
|
|
||||||
/// Send a 12-byte packet: 4 zero bytes + 8-byte identifier.
|
|
||||||
/// Receive a 24-byte response: 4 bytes version + 8 bytes identifier echo
|
|
||||||
/// + 4 bytes current_users + 4 bytes max_users + 4 bytes bandwidth.
|
|
||||||
#[cfg(feature = "networking")]
|
|
||||||
pub async fn ping_server(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
|
|
||||||
use color_eyre::eyre::{bail, eyre};
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::net::{lookup_host, UdpSocket};
|
|
||||||
|
|
||||||
let dest = lookup_host(format!("{}:{}", address, port))
|
|
||||||
.await?
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| eyre!("could not resolve address"))?;
|
|
||||||
|
|
||||||
let bind_addr = if dest.is_ipv6() { "[::]:0" } else { "0.0.0.0:0" };
|
|
||||||
let socket = UdpSocket::bind(bind_addr).await?;
|
|
||||||
socket.connect(dest).await?;
|
|
||||||
|
|
||||||
let request_id: u64 = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_nanos() as u64;
|
|
||||||
|
|
||||||
let mut buf = [0u8; 12];
|
|
||||||
buf[4..12].copy_from_slice(&request_id.to_be_bytes());
|
|
||||||
socket.send(&buf).await?;
|
|
||||||
|
|
||||||
let mut response = [0u8; 24];
|
|
||||||
let timeout = tokio::time::timeout(Duration::from_secs(2), socket.recv(&mut response)).await;
|
|
||||||
|
|
||||||
match timeout {
|
|
||||||
Ok(Ok(len)) if len >= 24 => {
|
|
||||||
let version_major = response[0] as u32;
|
|
||||||
let version_minor = response[1] as u32;
|
|
||||||
let version_patch = response[2] as u32;
|
|
||||||
let users =
|
|
||||||
u32::from_be_bytes([response[12], response[13], response[14], response[15]]);
|
|
||||||
let max_users =
|
|
||||||
u32::from_be_bytes([response[16], response[17], response[18], response[19]]);
|
|
||||||
let bandwidth =
|
|
||||||
u32::from_be_bytes([response[20], response[21], response[22], response[23]]);
|
|
||||||
|
|
||||||
Ok(ServerStatus {
|
|
||||||
success: true,
|
|
||||||
version: Some((version_major, version_minor, version_patch)),
|
|
||||||
users: Some(users),
|
|
||||||
max_users: Some(max_users),
|
|
||||||
bandwidth: Some(bandwidth),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Ok(Ok(_)) => bail!("ping response too short"),
|
|
||||||
Ok(Err(e)) => Err(e.into()),
|
|
||||||
Err(_) => bail!("ping timed out"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+3
-1
@@ -2,11 +2,13 @@ localhost:64444 {
|
|||||||
tls internal
|
tls internal
|
||||||
|
|
||||||
# Proxy /config path to mumble-web2-proxy
|
# Proxy /config path to mumble-web2-proxy
|
||||||
reverse_proxy /overrides http://127.0.0.1:4400
|
reverse_proxy /config http://127.0.0.1:4400
|
||||||
|
|
||||||
# Proxy /status path to mumble-web2-proxy
|
# Proxy /status path to mumble-web2-proxy
|
||||||
reverse_proxy /status http://127.0.0.1:4400
|
reverse_proxy /status http://127.0.0.1:4400
|
||||||
|
|
||||||
|
|
||||||
# Proxy root path to dx-serve
|
# Proxy root path to dx-serve
|
||||||
reverse_proxy http://127.0.0.1:8080
|
reverse_proxy http://127.0.0.1:8080
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
|
|
||||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
||||||
IMAGE_NAME="mumble-web2/android-release-builder:local"
|
|
||||||
|
|
||||||
TARGET="${1:-aarch64-linux-android}"
|
|
||||||
|
|
||||||
echo "==> Building Android builder Docker image..."
|
|
||||||
docker build -t "$IMAGE_NAME" -f "$SCRIPT_DIR/android-release-builder.Dockerfile" "$PROJECT_ROOT"
|
|
||||||
|
|
||||||
echo "==> Building Android APK (target: $TARGET)..."
|
|
||||||
docker run --rm \
|
|
||||||
-v "$PROJECT_ROOT:/app" \
|
|
||||||
-w /app \
|
|
||||||
"$IMAGE_NAME" \
|
|
||||||
dx build --platform android --target "$TARGET" --release -p mumble-web2-gui
|
|
||||||
|
|
||||||
echo "==> Done! APK should be at:"
|
|
||||||
echo " target/dx/mumble-web2-gui/release/android/app/app/build/outputs/apk/debug/app-debug.apk"
|
|
||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
# volumes:
|
# volumes:
|
||||||
# - ..:/app
|
# - ..:/app
|
||||||
# environment:
|
# environment:
|
||||||
# - MUMBLE_WEB2_PROXY_OVERRIDES_URL=https://localhost:64444/overrides
|
# - MUMBLE_WEB2_GUI_CONFIG_URL=https://localhost:64444/config
|
||||||
# stdin_open: true
|
# stdin_open: true
|
||||||
# tty: true
|
# tty: true
|
||||||
# command: >
|
# command: >
|
||||||
|
|||||||
+129
-6
@@ -4,11 +4,103 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# Web Dependencies
|
||||||
|
# ================
|
||||||
|
dioxus-web = { version = "0.7.1", optional = true }
|
||||||
|
wasm-bindgen = { version = "^0.2.92", optional = true }
|
||||||
|
wasm-bindgen-futures = { version = "^0.4.42", optional = true }
|
||||||
|
wasm-streams = { version = "^0.4.0", optional = true }
|
||||||
|
serde-wasm-bindgen = { version = "^0.6.5", optional = true }
|
||||||
|
js-sys = { version = "^0.3.70", optional = true }
|
||||||
|
web-sys = { version = "^0.3.72", features = [
|
||||||
|
"WebTransport",
|
||||||
|
"console",
|
||||||
|
"WebTransportOptions",
|
||||||
|
"WebTransportBidirectionalStream",
|
||||||
|
"WebTransportSendStream",
|
||||||
|
"WebTransportReceiveStream",
|
||||||
|
"Navigator",
|
||||||
|
"MediaDevices",
|
||||||
|
"AudioDecoder",
|
||||||
|
"AudioDecoderInit",
|
||||||
|
"AudioData",
|
||||||
|
"AudioEncoderConfig",
|
||||||
|
"AudioDecoderConfig",
|
||||||
|
"EncodedAudioChunk",
|
||||||
|
"EncodedAudioChunkInit",
|
||||||
|
"EncodedAudioChunkType",
|
||||||
|
"CodecState",
|
||||||
|
"AudioContext",
|
||||||
|
"AudioContextOptions",
|
||||||
|
"MediaStream",
|
||||||
|
"GainNode",
|
||||||
|
"MediaStreamAudioSourceNode",
|
||||||
|
"BaseAudioContext",
|
||||||
|
"AudioDestinationNode",
|
||||||
|
"AudioWorkletNode",
|
||||||
|
"AudioWorklet",
|
||||||
|
"AudioWorkletProcessor",
|
||||||
|
"MessagePort",
|
||||||
|
"MediaStreamConstraints",
|
||||||
|
"WorkletOptions",
|
||||||
|
"AudioEncoder",
|
||||||
|
"AudioEncoderInit",
|
||||||
|
"AudioDataInit",
|
||||||
|
"HtmlAnchorElement",
|
||||||
|
"Url",
|
||||||
|
"Blob",
|
||||||
|
"AudioDataCopyToOptions",
|
||||||
|
"AudioSampleFormat",
|
||||||
|
"Storage",
|
||||||
|
], optional = true }
|
||||||
|
gloo-timers = { version = "^0.3.0", features = ["futures"], optional = true }
|
||||||
|
tracing-web = { version = "^0.1.3", optional = true }
|
||||||
|
|
||||||
|
# Desktop Dependecies
|
||||||
|
# ===================
|
||||||
|
tokio = { version = "^1.41.1", features = ["net", "rt"], optional = true }
|
||||||
|
tokio-rustls = { version = "^0.26.0", optional = true }
|
||||||
|
opus = { version = "0.3.0", optional = true }
|
||||||
|
cpal = { version = "0.15.3", optional = true }
|
||||||
|
dasp_ring_buffer = { version = "0.11.0", optional = true }
|
||||||
|
etcetera = { version = "0.10.0", optional = true }
|
||||||
|
|
||||||
|
# Base Dependencies
|
||||||
|
# ================
|
||||||
dioxus = { version = "0.7.2" }
|
dioxus = { version = "0.7.2" }
|
||||||
dioxus-web = { version = "0.7.2", optional = true }
|
once_cell = "1.19.0"
|
||||||
mumble-web2-client = { version = "0.1.0", path = "../client" }
|
asynchronous-codec = { workspace = true }
|
||||||
mumble-web2-common = { version = "0.1.0", path = "../common" }
|
futures = "^0.3.30"
|
||||||
|
merge-io = "^0.3.0"
|
||||||
|
mumble-protocol = { workspace = true }
|
||||||
|
serde_json = "1"
|
||||||
|
tokio-util = { version = "^0.7.11", features = ["codec", "compat"] }
|
||||||
|
byteorder = "1.5"
|
||||||
|
ogg = "^0.9.1"
|
||||||
|
ordermap = "^0.5.3"
|
||||||
|
html-purifier = "^0.3.0"
|
||||||
|
markdown = "^0.3.0"
|
||||||
|
futures-channel = "^0.3.30"
|
||||||
|
mumble-web2-common = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
tracing-subscriber = { version = "^0.3.18", features = ["ansi"] }
|
||||||
|
tracing = "^0.1.40"
|
||||||
color-eyre = "^0.6.3"
|
color-eyre = "^0.6.3"
|
||||||
|
crossbeam-queue = "^0.3.11"
|
||||||
|
lol_html = "^2.2.0"
|
||||||
|
base64 = "^0.22"
|
||||||
|
mime_guess = "^2.0.5"
|
||||||
|
async_cell = "^0.2.3"
|
||||||
|
reqwest = { version = "^0.12.22", features = ["json"] }
|
||||||
|
dioxus-asset-resolver = "0.7.2"
|
||||||
|
|
||||||
|
|
||||||
|
# Denoising
|
||||||
|
# =========
|
||||||
|
deep_filter = { git = "https://github.com/Rikorose/DeepFilterNet.git", rev = "d375b2d8309e0935d165700c91da9de862a99c31", features = [
|
||||||
|
"tract",
|
||||||
|
] }
|
||||||
|
crossbeam = "0.8.4"
|
||||||
|
|
||||||
# Platform Integration
|
# Platform Integration
|
||||||
# ====================
|
# ====================
|
||||||
@@ -16,19 +108,50 @@ color-eyre = "^0.6.3"
|
|||||||
[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "macos", target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "macos", target_arch = "wasm32"))'.dependencies]
|
||||||
rfd = { git = "https://github.com/PolyMeilex/rfd.git", version = "^0.16.0", default-features = false, optional = true }
|
rfd = { git = "https://github.com/PolyMeilex/rfd.git", version = "^0.16.0", default-features = false, optional = true }
|
||||||
|
|
||||||
|
# Android dependencies for requesting permissions
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
android-permissions = "0.1.2"
|
||||||
|
jni = "0.21.1"
|
||||||
|
ndk-context = "0.1.1"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
tract-hir = "=0.12.4"
|
||||||
|
tract-core = "=0.12.4"
|
||||||
|
tract-onnx = "=0.12.4"
|
||||||
|
tract-pulse = "=0.12.4"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
web = [
|
web = [
|
||||||
"dioxus/web",
|
"dioxus/web",
|
||||||
"dioxus-web",
|
"dioxus-web",
|
||||||
"mumble-web2-client/web",
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"js-sys",
|
||||||
|
"web-sys",
|
||||||
|
"gloo-timers",
|
||||||
|
"tracing-web",
|
||||||
|
"deep_filter/wasm",
|
||||||
"rfd",
|
"rfd",
|
||||||
]
|
]
|
||||||
desktop = [
|
desktop = [
|
||||||
"dioxus/desktop",
|
"dioxus/desktop",
|
||||||
"mumble-web2-client/desktop",
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tracing-subscriber/env-filter",
|
||||||
|
"opus",
|
||||||
|
"cpal",
|
||||||
|
"dasp_ring_buffer",
|
||||||
"rfd/xdg-portal",
|
"rfd/xdg-portal",
|
||||||
|
"etcetera",
|
||||||
]
|
]
|
||||||
mobile = [
|
mobile = [
|
||||||
"dioxus/mobile",
|
"dioxus/mobile",
|
||||||
"mumble-web2-client/mobile"
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tracing-subscriber/env-filter",
|
||||||
|
"opus",
|
||||||
|
"cpal",
|
||||||
|
"dasp_ring_buffer",
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-19
@@ -16,7 +16,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#main {
|
#main {
|
||||||
visibility: visible;
|
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -582,7 +581,8 @@ a:visited {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|
||||||
/* Make this solid or nearly solid instead of see-through */
|
/* Make this solid or nearly solid instead of see-through */
|
||||||
background: #141414;
|
/* Old: background: rgba(255, 255, 255, 0.05); */
|
||||||
|
background: #141414; /* or #151822, or rgb(15, 15, 20) */
|
||||||
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
@@ -643,23 +643,6 @@ a:visited {
|
|||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25);
|
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-field input:user-invalid,
|
|
||||||
.modal-field--strict input:invalid {
|
|
||||||
border-color: rgba(255, 90, 90, 0.85);
|
|
||||||
box-shadow: 0 0 0 1px rgba(255, 90, 90, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-field__error {
|
|
||||||
display: none;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #ff8888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-field:has(input:user-invalid) .modal-field__error,
|
|
||||||
.modal-field--strict:has(input:invalid) .modal-field__error {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Actions row */
|
/* Actions row */
|
||||||
|
|
||||||
.modal-actions {
|
.modal-actions {
|
||||||
|
|||||||
+1182
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
use df::tract::{mut_slice_as_arrayviewmut, slice_as_arrayview};
|
use df::tract::{mut_slice_as_arrayviewmut, slice_as_arrayview};
|
||||||
use df::tract::{DfParams, DfTract, RuntimeParams};
|
use df::tract::{DfParams, DfTract, RuntimeParams};
|
||||||
|
use dioxus::prelude::{asset, manganis, Asset};
|
||||||
use dioxus_asset_resolver::read_asset_bytes;
|
use dioxus_asset_resolver::read_asset_bytes;
|
||||||
use manganis::{asset, Asset};
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
@@ -44,12 +44,11 @@ fn with_denoising_model<O>(spawn: &SpawnHandle, func: impl FnOnce(&mut DfTract)
|
|||||||
let cell = Arc::new(AtomicCell::new(None));
|
let cell = Arc::new(AtomicCell::new(None));
|
||||||
let cell_task = cell.clone();
|
let cell_task = cell.clone();
|
||||||
*state = DenoisingModelState::Downloading(cell);
|
*state = DenoisingModelState::Downloading(cell);
|
||||||
let model = DF_MODEL.to_string();
|
|
||||||
spawn.spawn(async move {
|
spawn.spawn(async move {
|
||||||
let model_bytes = match read_asset_bytes(&model).await {
|
let model_bytes = match read_asset_bytes(&DF_MODEL).await {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("could not read denoising model from \"{model}\": {e:?}");
|
error!("could not read denoising model from \"{DF_MODEL}\": {e:?}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -97,9 +96,20 @@ pub struct AudioProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AudioProcessor {
|
impl AudioProcessor {
|
||||||
pub fn new(denoise: bool) -> Self {
|
pub fn new_plain() -> Self {
|
||||||
AudioProcessor {
|
AudioProcessor {
|
||||||
denoise,
|
denoise: false,
|
||||||
|
spawn: SpawnHandle::current(),
|
||||||
|
buffer: Vec::new(),
|
||||||
|
noise_floor: DEFAULT_NOISE_FLOOR,
|
||||||
|
was_transmitting: false,
|
||||||
|
hold_samples: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_denoising() -> Self {
|
||||||
|
AudioProcessor {
|
||||||
|
denoise: true,
|
||||||
spawn: SpawnHandle::current(),
|
spawn: SpawnHandle::current(),
|
||||||
buffer: Vec::new(),
|
buffer: Vec::new(),
|
||||||
noise_floor: DEFAULT_NOISE_FLOOR,
|
noise_floor: DEFAULT_NOISE_FLOOR,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::app::{Command, ConnectTarget, SharedState};
|
use crate::app::Command;
|
||||||
use color_eyre::eyre::{bail, Error};
|
use color_eyre::eyre::{bail, Error};
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_protocol::control::ClientControlCodec;
|
use mumble_protocol::control::ClientControlCodec;
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -8,13 +8,13 @@ use tokio::net::TcpStream;
|
|||||||
use tokio_rustls::rustls;
|
use tokio_rustls::rustls;
|
||||||
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
||||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||||
use tokio_rustls::rustls::ClientConfig;
|
use tokio_rustls::rustls::ClientConfig as RlsClientConfig;
|
||||||
use tokio_rustls::rustls::DigitallySignedStruct;
|
use tokio_rustls::rustls::DigitallySignedStruct;
|
||||||
use tokio_rustls::TlsConnector;
|
use tokio_rustls::TlsConnector;
|
||||||
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
|
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
|
||||||
use tracing::{info, instrument};
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
use mumble_web2_common::ProxyOverrides;
|
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct NoCertificateVerification;
|
struct NoCertificateVerification;
|
||||||
@@ -70,36 +70,29 @@ impl ServerCertVerifier for NoCertificateVerification {
|
|||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
pub async fn network_connect(
|
pub async fn network_connect(
|
||||||
target: ConnectTarget,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
overrides: &ProxyOverrides,
|
gui_config: &ClientConfig,
|
||||||
state: SharedState,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
info!("connecting");
|
info!("connecting");
|
||||||
|
|
||||||
let (host, port) = match target {
|
let config = RlsClientConfig::builder()
|
||||||
ConnectTarget::Direct { host, port } => (host, port),
|
|
||||||
ConnectTarget::Proxy(_) => {
|
|
||||||
bail!("desktop/mobile platform requires a direct host:port, not a proxy URL")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = ClientConfig::builder()
|
|
||||||
.dangerous()
|
.dangerous()
|
||||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||||
.with_no_client_auth();
|
.with_no_client_auth();
|
||||||
|
|
||||||
let connector = TlsConnector::from(Arc::new(config));
|
let connector = TlsConnector::from(Arc::new(config));
|
||||||
|
|
||||||
let addr = (&*host, port)
|
let addr = format!("{}:{}", address, 64738)
|
||||||
.to_socket_addrs()?
|
.to_socket_addrs()?
|
||||||
.next()
|
.next()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let server_tcp = TcpStream::connect(addr).await?;
|
let server_tcp = TcpStream::connect(addr).await?;
|
||||||
let server_stream = connector
|
let server_stream = connector
|
||||||
.connect(host.try_into()?, server_tcp)
|
//.connect("127.0.0.1".try_into()?, server_tcp)
|
||||||
|
.connect(address.try_into()?, server_tcp)
|
||||||
.await?;
|
.await?;
|
||||||
let (read_server, write_server) = tokio::io::split(server_stream);
|
let (read_server, write_server) = tokio::io::split(server_stream);
|
||||||
|
|
||||||
@@ -109,9 +102,11 @@ pub async fn network_connect(
|
|||||||
let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
|
let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
|
||||||
let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
|
let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
|
||||||
|
|
||||||
let (outgoing_send, outgoing_recv) = futures_channel::mpsc::unbounded();
|
crate::network_loop(username, event_rx, reader, writer).await
|
||||||
spawn(crate::sender_loop(outgoing_recv, writer));
|
}
|
||||||
crate::network_loop(username, state, event_rx, outgoing_send, reader).await
|
|
||||||
|
pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||||
|
bail!("status not supported on desktop yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
use crate::app::Command;
|
||||||
|
use color_eyre::eyre::{bail, Error};
|
||||||
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
|
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
||||||
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Desktop platform implementation using Tokio and native audio.
|
||||||
|
pub struct DesktopPlatform;
|
||||||
|
|
||||||
|
impl super::PlatformInterface for DesktopPlatform {
|
||||||
|
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||||
|
|
||||||
|
async fn sleep(duration: Duration) {
|
||||||
|
tokio::time::sleep(duration).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||||
|
Ok(ClientConfig {
|
||||||
|
proxy_url: None,
|
||||||
|
cert_hash: None,
|
||||||
|
any_server: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_username() -> Option<String> {
|
||||||
|
let config = load_config_map();
|
||||||
|
config.get("username").cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_server_url() -> Option<String> {
|
||||||
|
let config = load_config_map();
|
||||||
|
config.get("server").cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
let config = load_config_map();
|
||||||
|
config
|
||||||
|
.get("servers")
|
||||||
|
.and_then(|s| serde_json::from_str(s).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(servers: &[ServerEntry]) {
|
||||||
|
let mut config = load_config_map();
|
||||||
|
if let Ok(json) = serde_json::to_string(servers) {
|
||||||
|
config.insert("servers".to_string(), json);
|
||||||
|
let _ = save_config_map(&config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn network_connect(
|
||||||
|
address: String,
|
||||||
|
username: String,
|
||||||
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
|
gui_config: &ClientConfig,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
super::connect::network_connect(address, username, event_rx, gui_config).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||||
|
super::connect::get_status(client).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ping_server(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
|
||||||
|
mumble_udp_ping(address, port).await
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_permissions() {
|
||||||
|
// No-op on desktop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_config_path() -> std::path::PathBuf {
|
||||||
|
let strategy = choose_app_strategy(AppStrategyArgs {
|
||||||
|
top_level_domain: "xyz".to_string(),
|
||||||
|
author: "ohea".to_string(),
|
||||||
|
app_name: "Mumble Web2".to_string(),
|
||||||
|
})
|
||||||
|
.expect("failed to choose app strategy");
|
||||||
|
strategy.config_dir().join("config.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_config_map() -> HashMap<String, String> {
|
||||||
|
let config_path = get_config_path();
|
||||||
|
match std::fs::read_to_string(&config_path) {
|
||||||
|
Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(),
|
||||||
|
Err(_) => HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_config_map(config: &HashMap<String, String>) -> color_eyre::Result<()> {
|
||||||
|
let config_path = get_config_path();
|
||||||
|
if let Some(parent) = config_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let contents = serde_json::to_string_pretty(config)?;
|
||||||
|
std::fs::write(&config_path, contents)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mumble UDP ping protocol.
|
||||||
|
///
|
||||||
|
/// Send a 12-byte packet: 4 zero bytes + 8-byte identifier.
|
||||||
|
/// Receive a 24-byte response: 4 bytes version (1 byte each: major.minor.patch + padding)
|
||||||
|
/// + 8 bytes identifier echo + 4 bytes current_users + 4 bytes max_users + 4 bytes bandwidth.
|
||||||
|
async fn mumble_udp_ping(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
let dest = format!("{}:{}", address, port)
|
||||||
|
.to_socket_addrs()?
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| color_eyre::eyre::eyre!("could not resolve address"))?;
|
||||||
|
|
||||||
|
let bind_addr = if dest.is_ipv6() { "[::]:0" } else { "0.0.0.0:0" };
|
||||||
|
let socket = UdpSocket::bind(bind_addr).await?;
|
||||||
|
socket.connect(dest).await?;
|
||||||
|
|
||||||
|
// Build ping packet: 4 zero bytes + 8-byte request ID
|
||||||
|
let request_id: u64 = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_nanos() as u64;
|
||||||
|
|
||||||
|
let mut buf = [0u8; 12];
|
||||||
|
buf[4..12].copy_from_slice(&request_id.to_be_bytes());
|
||||||
|
socket.send(&buf).await?;
|
||||||
|
|
||||||
|
let mut response = [0u8; 24];
|
||||||
|
let timeout = tokio::time::timeout(Duration::from_secs(2), socket.recv(&mut response)).await;
|
||||||
|
|
||||||
|
match timeout {
|
||||||
|
Ok(Ok(len)) if len >= 24 => {
|
||||||
|
let version_major = response[0] as u32;
|
||||||
|
let version_minor = response[1] as u32;
|
||||||
|
let version_patch = response[2] as u32;
|
||||||
|
let users = u32::from_be_bytes([response[12], response[13], response[14], response[15]]);
|
||||||
|
let max_users = u32::from_be_bytes([response[16], response[17], response[18], response[19]]);
|
||||||
|
let bandwidth = u32::from_be_bytes([response[20], response[21], response[22], response[23]]);
|
||||||
|
|
||||||
|
Ok(ServerStatus {
|
||||||
|
success: true,
|
||||||
|
version: Some((version_major, version_minor, version_patch)),
|
||||||
|
users: Some(users),
|
||||||
|
max_users: Some(max_users),
|
||||||
|
bandwidth: Some(bandwidth),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(Ok(_)) => bail!("ping response too short"),
|
||||||
|
Ok(Err(e)) => Err(e.into()),
|
||||||
|
Err(_) => bail!("ping timed out"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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 mumble_web2_common::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -31,10 +31,16 @@ impl super::PlatformInterface for MobilePlatform {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_default_server(server: &str) -> Option<()> {
|
fn set_default_server(_server: &str) -> Option<()> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(_servers: &[ServerEntry]) {}
|
||||||
|
|
||||||
async fn network_connect(
|
async fn network_connect(
|
||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
@@ -48,6 +54,10 @@ impl super::PlatformInterface for MobilePlatform {
|
|||||||
super::connect::get_status(client).await
|
super::connect::get_status(client).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ping_server(_address: &str, _port: u16) -> color_eyre::Result<ServerStatus> {
|
||||||
|
color_eyre::eyre::bail!("ping not supported on mobile yet")
|
||||||
|
}
|
||||||
|
|
||||||
fn init_logging() {
|
fn init_logging() {
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
use tracing_subscriber::filter::EnvFilter;
|
use tracing_subscriber::filter::EnvFilter;
|
||||||
@@ -4,12 +4,10 @@
|
|||||||
//! The traits make the platform boundary explicit and provide compile-time verification.
|
//! The traits make the platform boundary explicit and provide compile-time verification.
|
||||||
#![allow(async_fn_in_trait)]
|
#![allow(async_fn_in_trait)]
|
||||||
|
|
||||||
use crate::app::{Command, ConnectTarget, SharedState};
|
use crate::{app::Command, effects::AudioProcessor};
|
||||||
use crate::effects::AudioProcessor;
|
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -52,24 +50,11 @@ pub trait AudioPlayerInterface {
|
|||||||
fn play_opus(&mut self, payload: &[u8]);
|
fn play_opus(&mut self, payload: &[u8]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ConfigSystemInterface: Sized + Clone {
|
|
||||||
fn new() -> Result<Self, Error>;
|
|
||||||
|
|
||||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
|
||||||
where
|
|
||||||
T: serde::de::DeserializeOwned;
|
|
||||||
|
|
||||||
fn config_set<T>(&self, key: &str, value: &T)
|
|
||||||
where
|
|
||||||
T: serde::Serialize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is the main trait that each platform must implement. It combines all
|
/// This is the main trait that each platform must implement. It combines all
|
||||||
/// platform-specific functionality into a single interface, providing compile-time
|
/// platform-specific functionality into a single interface, providing compile-time
|
||||||
/// 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 ConfigSystem: ConfigSystemInterface;
|
|
||||||
|
|
||||||
/// Initialize logging for the platform.
|
/// Initialize logging for the platform.
|
||||||
fn init_logging();
|
fn init_logging();
|
||||||
@@ -79,25 +64,43 @@ pub trait PlatformInterface {
|
|||||||
|
|
||||||
/// Establish a connection to the Mumble server and run the network loop.
|
/// Establish a connection to the Mumble server and run the network loop.
|
||||||
fn network_connect(
|
fn network_connect(
|
||||||
target: ConnectTarget,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
proxy_overrides: &ProxyOverrides,
|
gui_config: &ClientConfig,
|
||||||
state: SharedState,
|
|
||||||
) -> impl Future<Output = Result<(), Error>>;
|
) -> impl Future<Output = Result<(), Error>>;
|
||||||
|
|
||||||
/// Get server status (user count, version, etc.) for the given address.
|
/// Get server status (user count, version, etc.) via the web proxy status endpoint.
|
||||||
///
|
|
||||||
/// On web, this goes through the proxy's /status endpoint and ignores `address`
|
|
||||||
/// (the proxy is bound to a specific server). On desktop/mobile, this pings the
|
|
||||||
/// given address directly via UDP.
|
|
||||||
fn get_status(
|
fn get_status(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||||
|
|
||||||
|
/// Ping a mumble server via UDP to get version, user count, etc.
|
||||||
|
fn ping_server(
|
||||||
address: &str,
|
address: &str,
|
||||||
|
port: u16,
|
||||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||||
|
|
||||||
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
||||||
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>>;
|
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>>;
|
||||||
|
|
||||||
|
/// Load saved username.
|
||||||
|
fn load_username() -> Option<String>;
|
||||||
|
|
||||||
|
/// Load saved server URL.
|
||||||
|
fn load_server_url() -> Option<String>;
|
||||||
|
|
||||||
|
/// Save the default username.
|
||||||
|
fn set_default_username(username: &str) -> Option<()>;
|
||||||
|
|
||||||
|
/// Save the default server URL.
|
||||||
|
fn set_default_server(server: &str) -> Option<()>;
|
||||||
|
|
||||||
|
/// Load the saved server list.
|
||||||
|
fn load_servers() -> Vec<ServerEntry>;
|
||||||
|
|
||||||
|
/// Save the server list.
|
||||||
|
fn save_servers(servers: &[ServerEntry]);
|
||||||
|
|
||||||
/// 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 = ()>;
|
||||||
@@ -107,21 +110,15 @@ pub trait PlatformInterface {
|
|||||||
// Platform Modules
|
// Platform Modules
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
mod stub;
|
|
||||||
|
|
||||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||||
mod connect;
|
mod connect;
|
||||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
|
||||||
mod native_audio;
|
|
||||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
|
||||||
mod native_config;
|
|
||||||
|
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
mod desktop;
|
mod desktop;
|
||||||
|
|
||||||
#[cfg(feature = "mobile")]
|
#[cfg(feature = "mobile")]
|
||||||
mod mobile;
|
mod mobile;
|
||||||
|
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||||
|
mod native_audio;
|
||||||
|
mod stub;
|
||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
@@ -148,8 +145,6 @@ pub type Platform = stub::StubPlatform;
|
|||||||
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 ConfigSystem = <Platform as PlatformInterface>::ConfigSystem;
|
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
// Platform Async Runtime
|
// Platform Async Runtime
|
||||||
// ========================
|
// ========================
|
||||||
@@ -181,12 +176,3 @@ const _: () = {
|
|||||||
let _ = assert_platform::<mobile::MobilePlatform>;
|
let _ = assert_platform::<mobile::MobilePlatform>;
|
||||||
let _ = assert_platform::<stub::StubPlatform>;
|
let _ = assert_platform::<stub::StubPlatform>;
|
||||||
};
|
};
|
||||||
|
|
||||||
fn global_default_config() -> HashMap<String, serde_json::Value> {
|
|
||||||
serde_json::json!({})
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
@@ -112,7 +112,7 @@ impl super::AudioSystemInterface for NativeAudioSystem {
|
|||||||
);
|
);
|
||||||
let mut encoder =
|
let mut encoder =
|
||||||
opus::Encoder::new(SAMPLE_RATE, opus::Channels::Mono, opus::Application::Voip)?;
|
opus::Encoder::new(SAMPLE_RATE, opus::Channels::Mono, opus::Application::Voip)?;
|
||||||
let mut current_processor = AudioProcessor::new(false);
|
let mut current_processor = AudioProcessor::new_plain();
|
||||||
let mut output_buffer = Vec::new();
|
let mut output_buffer = Vec::new();
|
||||||
let processors = self.processors.clone();
|
let processors = self.processors.clone();
|
||||||
let error_callback = move |e: cpal::StreamError| error!("error recording: {e:?}");
|
let error_callback = move |e: cpal::StreamError| error!("error recording: {e:?}");
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
/// Stub implementation of the platform interface, so that we can
|
/// Stub implementation of the platform interface, so that we can
|
||||||
/// `cargo check` without any --feature flags.
|
/// `cargo check` without any --feature flags.
|
||||||
use crate::{
|
use crate::effects::AudioProcessor;
|
||||||
app::{ConnectTarget, SharedState},
|
|
||||||
effects::AudioProcessor,
|
|
||||||
};
|
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
pub struct StubPlatform;
|
pub struct StubPlatform;
|
||||||
|
|
||||||
impl super::PlatformInterface for StubPlatform {
|
impl super::PlatformInterface for StubPlatform {
|
||||||
type AudioSystem = StubAudioSystem;
|
type AudioSystem = StubAudioSystem;
|
||||||
type ConfigSystem = StubConfigSystem;
|
|
||||||
|
|
||||||
fn init_logging() {
|
fn init_logging() {
|
||||||
panic!("stubbed platform")
|
panic!("stubbed platform")
|
||||||
@@ -24,26 +20,55 @@ impl super::PlatformInterface for StubPlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn network_connect(
|
fn network_connect(
|
||||||
_target: ConnectTarget,
|
_address: String,
|
||||||
_username: String,
|
_username: String,
|
||||||
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
||||||
_overrides: &ProxyOverrides,
|
_gui_config: &ClientConfig,
|
||||||
_state: SharedState,
|
|
||||||
) -> impl Future<Output = Result<(), Error>> {
|
) -> impl Future<Output = Result<(), Error>> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_status(
|
fn get_status(
|
||||||
_client: &reqwest::Client,
|
_client: &reqwest::Client,
|
||||||
_address: &str,
|
|
||||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>> {
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>> {
|
fn ping_server(
|
||||||
|
_address: &str,
|
||||||
|
_port: u16,
|
||||||
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>> {
|
||||||
|
async { panic!("stubbed platform") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_username() -> Option<String> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_server_url() -> Option<String> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_default_username(_username: &str) -> Option<()> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_default_server(_server: &str) -> Option<()> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(_servers: &[ServerEntry]) {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
fn sleep(_duration: std::time::Duration) -> impl Future<Output = ()> {
|
fn sleep(_duration: std::time::Duration) -> impl Future<Output = ()> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
@@ -82,29 +107,6 @@ impl super::AudioPlayerInterface for StubAudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct StubConfigSystem;
|
|
||||||
|
|
||||||
impl super::ConfigSystemInterface for StubConfigSystem {
|
|
||||||
fn new() -> Result<Self, Error> {
|
|
||||||
panic!("stubbed platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
|
||||||
where
|
|
||||||
T: serde::de::DeserializeOwned,
|
|
||||||
{
|
|
||||||
panic!("stubbed platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_set<T>(&self, key: &str, value: &T)
|
|
||||||
where
|
|
||||||
T: serde::Serialize,
|
|
||||||
{
|
|
||||||
panic!("stubbed platform")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct SpawnHandle;
|
pub struct SpawnHandle;
|
||||||
|
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
use crate::app::{Command, ConnectTarget, SharedState};
|
use crate::app::Command;
|
||||||
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
|
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
|
||||||
use color_eyre::eyre::{bail, eyre, Error};
|
use color_eyre::eyre::{bail, eyre, Error};
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
use dioxus::prelude::*;
|
||||||
use gloo_timers::future::TimeoutFuture;
|
use gloo_timers::future::TimeoutFuture;
|
||||||
use js_sys::Float32Array;
|
use js_sys::Float32Array;
|
||||||
use manganis::asset;
|
|
||||||
use mumble_protocol::control::ClientControlCodec;
|
use mumble_protocol::control::ClientControlCodec;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -64,7 +62,6 @@ pub struct WebPlatform;
|
|||||||
|
|
||||||
impl super::PlatformInterface for WebPlatform {
|
impl super::PlatformInterface for WebPlatform {
|
||||||
type AudioSystem = WebAudioSystem;
|
type AudioSystem = WebAudioSystem;
|
||||||
type ConfigSystem = WebConfigSystem;
|
|
||||||
|
|
||||||
fn init_logging() {
|
fn init_logging() {
|
||||||
// copied from tracing_web example usage
|
// copied from tracing_web example usage
|
||||||
@@ -92,41 +89,74 @@ impl super::PlatformInterface for WebPlatform {
|
|||||||
// No-op on web
|
// No-op on web
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||||
let overrides = match option_env!("MUMBLE_WEB2_PROXY_OVERRIDES_URL") {
|
let config_url = match option_env!("MUMBLE_WEB2_GUI_CONFIG_URL") {
|
||||||
Some(url) => Url::parse(url)?,
|
Some(url) => Url::parse(url)?,
|
||||||
None => absolute_url("overrides")?,
|
None => absolute_url("config")?,
|
||||||
};
|
};
|
||||||
info!("loading config from {}", overrides);
|
info!("loading config from {}", config_url);
|
||||||
|
|
||||||
let config = reqwest::get(overrides)
|
let config = reqwest::get(config_url)
|
||||||
.await?
|
.await?
|
||||||
.json::<ProxyOverrides>()
|
.json::<ClientConfig>()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn network_connect(
|
fn load_username() -> Option<String> {
|
||||||
target: ConnectTarget,
|
web_sys::window()
|
||||||
username: String,
|
.unwrap()
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
.local_storage()
|
||||||
overrides: &ProxyOverrides,
|
.ok()??
|
||||||
state: SharedState,
|
.get_item("username")
|
||||||
) -> Result<(), Error> {
|
.ok()?
|
||||||
let url = match target {
|
|
||||||
ConnectTarget::Proxy(url) => url,
|
|
||||||
ConnectTarget::Direct { .. } => {
|
|
||||||
bail!("web platform requires a proxy URL, not a direct host:port")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
network_connect(url, username, event_rx, overrides, state).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_status(
|
fn load_server_url() -> Option<String> {
|
||||||
client: &reqwest::Client,
|
None
|
||||||
_address: &str,
|
}
|
||||||
) -> color_eyre::Result<ServerStatus> {
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
web_sys::window()
|
||||||
|
.and_then(|w| w.local_storage().ok()?)
|
||||||
|
.and_then(|s| s.get_item("servers").ok()?)
|
||||||
|
.and_then(|json| serde_json::from_str(&json).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(servers: &[ServerEntry]) {
|
||||||
|
if let Ok(json) = serde_json::to_string(servers) {
|
||||||
|
if let Some(storage) = web_sys::window()
|
||||||
|
.and_then(|w| w.local_storage().ok()?)
|
||||||
|
{
|
||||||
|
let _ = storage.set_item("servers", &json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
Ok(client
|
||||||
.get(absolute_url("status")?)
|
.get(absolute_url("status")?)
|
||||||
.send()
|
.send()
|
||||||
@@ -135,6 +165,11 @@ impl super::PlatformInterface for WebPlatform {
|
|||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ping_server(_address: &str, _port: u16) -> color_eyre::Result<ServerStatus> {
|
||||||
|
// UDP ping not available in browsers; use get_status via HTTP proxy instead
|
||||||
|
color_eyre::eyre::bail!("UDP ping not supported on web platform")
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -167,7 +202,7 @@ pub struct WebAudioSystem {
|
|||||||
processors: AudioProcessorSender,
|
processors: AudioProcessorSender,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn attach_worklet(audio_context: &AudioContext, worklet_url: &str) -> Result<(), Error> {
|
async fn attach_worklet(audio_context: &AudioContext) -> Result<(), Error> {
|
||||||
// Create worklets to process mic and speaker audio
|
// Create worklets to process mic and speaker audio
|
||||||
// Speaker audio processing worklet only required on
|
// Speaker audio processing worklet only required on
|
||||||
// browsers that don't support MediaStreamTrackGenerator
|
// browsers that don't support MediaStreamTrackGenerator
|
||||||
@@ -180,11 +215,12 @@ async fn attach_worklet(audio_context: &AudioContext, worklet_url: &str) -> Resu
|
|||||||
)
|
)
|
||||||
.ey()?;
|
.ey()?;
|
||||||
|
|
||||||
info!("loading mic worklet from {worklet_url:?}");
|
let module = asset!("assets/rust_audio_worklet.js").to_string();
|
||||||
|
info!("loading mic worklet from {module:?}");
|
||||||
audio_context
|
audio_context
|
||||||
.audio_worklet()
|
.audio_worklet()
|
||||||
.ey()?
|
.ey()?
|
||||||
.add_module_with_options(worklet_url, &options)
|
.add_module_with_options(&module, &options)
|
||||||
.ey()?
|
.ey()?
|
||||||
.into_future()
|
.into_future()
|
||||||
.await
|
.await
|
||||||
@@ -199,11 +235,7 @@ impl super::AudioSystemInterface for WebAudioSystem {
|
|||||||
// Create MediaStreams to playback decoded audio
|
// Create MediaStreams to playback decoded audio
|
||||||
// The audio context is used to reproduce audio.
|
// The audio context is used to reproduce audio.
|
||||||
let webctx = configure_audio_context();
|
let webctx = configure_audio_context();
|
||||||
attach_worklet(
|
attach_worklet(&webctx).await?;
|
||||||
&webctx,
|
|
||||||
&asset!("/assets/rust_audio_worklet.js").to_string(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let processors = AudioProcessorSender::default();
|
let processors = AudioProcessorSender::default();
|
||||||
|
|
||||||
@@ -394,7 +426,7 @@ async fn run_encoder_worklet(
|
|||||||
audio_encoder.configure(&encoder_config);
|
audio_encoder.configure(&encoder_config);
|
||||||
info!("created audio encoder");
|
info!("created audio encoder");
|
||||||
|
|
||||||
let mut current_processor = AudioProcessor::new(false);
|
let mut current_processor = AudioProcessor::new_plain();
|
||||||
let onmessage: Closure<dyn FnMut(MessageEvent)> = Closure::new(move |event: MessageEvent| {
|
let onmessage: Closure<dyn FnMut(MessageEvent)> = Closure::new(move |event: MessageEvent| {
|
||||||
if let Some(new_processor) = processors.take() {
|
if let Some(new_processor) = processors.take() {
|
||||||
current_processor = new_processor;
|
current_processor = new_processor;
|
||||||
@@ -447,8 +479,7 @@ pub async fn network_connect(
|
|||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
overrides: &ProxyOverrides,
|
gui_config: &ClientConfig,
|
||||||
state: SharedState,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
info!("connecting");
|
info!("connecting");
|
||||||
|
|
||||||
@@ -461,7 +492,7 @@ pub async fn network_connect(
|
|||||||
)
|
)
|
||||||
.ey()?;
|
.ey()?;
|
||||||
|
|
||||||
if let Some(server_hash) = &overrides.cert_hash {
|
if let Some(server_hash) = &gui_config.cert_hash {
|
||||||
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
|
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
|
||||||
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?;
|
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?;
|
||||||
}
|
}
|
||||||
@@ -507,9 +538,7 @@ pub async fn network_connect(
|
|||||||
let writer =
|
let writer =
|
||||||
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec);
|
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec);
|
||||||
|
|
||||||
let (outgoing_send, outgoing_recv) = futures_channel::mpsc::unbounded();
|
crate::network_loop(username, event_rx, reader, writer).await
|
||||||
spawn(crate::sender_loop(outgoing_recv, writer));
|
|
||||||
crate::network_loop(username, state, event_rx, outgoing_send, reader).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
||||||
@@ -517,64 +546,3 @@ pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
|||||||
let location = window.location();
|
let location = window.location();
|
||||||
Ok(Url::parse(&location.href().ey()?)?.join(path)?)
|
Ok(Url::parse(&location.href().ey()?)?.join(path)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub struct WebConfigSystem {}
|
|
||||||
|
|
||||||
impl super::ConfigSystemInterface for WebConfigSystem {
|
|
||||||
fn new() -> Result<Self, Error> {
|
|
||||||
return Ok(WebConfigSystem {});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
|
||||||
where
|
|
||||||
T: serde::de::DeserializeOwned,
|
|
||||||
{
|
|
||||||
// Get Storage
|
|
||||||
let storage = web_sys::window()?.local_storage().ok()??;
|
|
||||||
|
|
||||||
// Try localStorage first
|
|
||||||
if let Ok(Some(raw)) = storage.get_item(key) {
|
|
||||||
if let Ok(parsed) = serde_json::from_str::<T>(&raw) {
|
|
||||||
return Some(parsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to default if deserialization fails or key missing
|
|
||||||
let default_value = config_get_default(key)?;
|
|
||||||
serde_json::from_value::<T>(default_value).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_set<T>(&self, key: &str, value: &T)
|
|
||||||
where
|
|
||||||
T: serde::Serialize,
|
|
||||||
{
|
|
||||||
let storage = window()
|
|
||||||
.and_then(|w| w.local_storage().ok().flatten())
|
|
||||||
.expect("localStorage not available");
|
|
||||||
|
|
||||||
let json_value =
|
|
||||||
serde_json::to_string(value).expect("failed to serialize config value to JSON string");
|
|
||||||
|
|
||||||
storage
|
|
||||||
.set_item(key, &json_value)
|
|
||||||
.expect("failed to write to localStorage");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_get_default(key: &str) -> Option<serde_json::Value> {
|
|
||||||
let default_config = platform_default_config();
|
|
||||||
default_config
|
|
||||||
.get(key)
|
|
||||||
.cloned()
|
|
||||||
.or(super::global_default_config().get(key).cloned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn platform_default_config() -> HashMap<String, serde_json::Value> {
|
|
||||||
serde_json::json!({})
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
use crate::msghtml::process_message_html;
|
use app::Chat;
|
||||||
use crate::AudioSettings;
|
use app::Command;
|
||||||
use crate::Chat;
|
use app::ConnectionState;
|
||||||
use crate::Command;
|
use app::STATE;
|
||||||
use crate::ConnectionState;
|
|
||||||
use asynchronous_codec::FramedRead;
|
use asynchronous_codec::FramedRead;
|
||||||
use asynchronous_codec::FramedWrite;
|
use asynchronous_codec::FramedWrite;
|
||||||
use color_eyre::eyre::{bail, Error};
|
use color_eyre::eyre::{bail, Error};
|
||||||
use dioxus_signals::ReadableExt as _;
|
use dioxus::prelude::*;
|
||||||
use dioxus_signals::WritableExt as _;
|
|
||||||
use futures::select;
|
use futures::select;
|
||||||
use futures::AsyncRead;
|
use futures::AsyncRead;
|
||||||
use futures::AsyncWrite;
|
use futures::AsyncWrite;
|
||||||
use futures::FutureExt as _;
|
use futures::FutureExt as _;
|
||||||
use futures::SinkExt as _;
|
use futures::SinkExt as _;
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
use futures_channel::mpsc::UnboundedSender;
|
||||||
|
use msghtml::process_message_html;
|
||||||
use mumble_protocol::control::msgs;
|
use mumble_protocol::control::msgs;
|
||||||
use mumble_protocol::control::ControlCodec;
|
use mumble_protocol::control::ControlCodec;
|
||||||
use mumble_protocol::control::ControlPacket;
|
use mumble_protocol::control::ControlPacket;
|
||||||
@@ -28,18 +27,21 @@ use std::time::Duration;
|
|||||||
use tracing::error;
|
use tracing::error;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::app::SharedState;
|
|
||||||
use crate::app::State;
|
|
||||||
use crate::effects::AudioProcessor;
|
use crate::effects::AudioProcessor;
|
||||||
use crate::imp::{
|
use crate::imp::{
|
||||||
spawn, AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _,
|
AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _, Platform,
|
||||||
Platform, PlatformInterface as _,
|
PlatformInterface as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state: SharedState) {
|
pub mod app;
|
||||||
|
mod effects;
|
||||||
|
pub mod imp;
|
||||||
|
mod msghtml;
|
||||||
|
|
||||||
|
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
|
||||||
loop {
|
loop {
|
||||||
let Some(Command::Connect {
|
let Some(Command::Connect {
|
||||||
target,
|
address,
|
||||||
username,
|
username,
|
||||||
config,
|
config,
|
||||||
}) = event_rx.next().await
|
}) = event_rx.next().await
|
||||||
@@ -47,25 +49,28 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state:
|
|||||||
panic!("did not receive connect command")
|
panic!("did not receive connect command")
|
||||||
};
|
};
|
||||||
|
|
||||||
*state.server.write_unchecked() = Default::default();
|
*STATE.server.write() = Default::default();
|
||||||
*state.status.write_unchecked() = ConnectionState::Connecting;
|
*STATE.status.write() = ConnectionState::Connecting;
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
Platform::network_connect(target, username, &mut event_rx, &config, state.clone())
|
Platform::network_connect(address, username, &mut event_rx, &config).await
|
||||||
.await
|
|
||||||
{
|
{
|
||||||
error!("could not connect {:?}", error);
|
error!("could not connect {:?}", error);
|
||||||
*state.status.write_unchecked() = ConnectionState::Failed(error.to_string());
|
*STATE.status.write() = ConnectionState::Failed(error.to_string());
|
||||||
} else {
|
} else {
|
||||||
*state.status.write_unchecked() = ConnectionState::Disconnected;
|
*STATE.status.write() = ConnectionState::Disconnected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn sender_loop<W: AsyncWrite + Unpin + 'static>(
|
pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin + 'static>(
|
||||||
mut outgoing: UnboundedReceiver<ControlPacket<Serverbound>>,
|
username: String,
|
||||||
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
|
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
||||||
mut writer: FramedWrite<W, ControlCodec<Serverbound, Clientbound>>,
|
mut writer: FramedWrite<W, ControlCodec<Serverbound, Clientbound>>,
|
||||||
) {
|
) -> Result<(), Error> {
|
||||||
while let Some(msg) = outgoing.next().await {
|
let (mut send_chan, mut writer_recv_chan) = futures_channel::mpsc::unbounded();
|
||||||
|
spawn(async move {
|
||||||
|
while let Some(msg) = writer_recv_chan.next().await {
|
||||||
if !matches!(msg, ControlPacket::Ping(_) | ControlPacket::UDPTunnel(_)) {
|
if !matches!(msg, ControlPacket::Ping(_) | ControlPacket::UDPTunnel(_)) {
|
||||||
info!("sending packet {:#?}", msg);
|
info!("sending packet {:#?}", msg);
|
||||||
}
|
}
|
||||||
@@ -74,16 +79,7 @@ pub(crate) async fn sender_loop<W: AsyncWrite + Unpin + 'static>(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
|
|
||||||
username: String,
|
|
||||||
state: SharedState,
|
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
|
||||||
mut outgoing: UnboundedSender<ControlPacket<Serverbound>>,
|
|
||||||
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let audio_settings = state.audio.read().clone();
|
|
||||||
|
|
||||||
// Get version packet
|
// Get version packet
|
||||||
let version = match reader.next().await {
|
let version = match reader.next().await {
|
||||||
@@ -98,17 +94,17 @@ pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
|
|||||||
msg.set_version(0x000010204);
|
msg.set_version(0x000010204);
|
||||||
msg.set_release(format!("{} {}", "mumbleweb2", "6.9.0"));
|
msg.set_release(format!("{} {}", "mumbleweb2", "6.9.0"));
|
||||||
//msg.set_os("Chrome".to_string());
|
//msg.set_os("Chrome".to_string());
|
||||||
outgoing.send(msg.into()).await.unwrap();
|
send_chan.send(msg.into()).await.unwrap();
|
||||||
|
|
||||||
// Send authenticate packet
|
// Send authenticate packet
|
||||||
let mut msg = msgs::Authenticate::new();
|
let mut msg = msgs::Authenticate::new();
|
||||||
msg.set_username(username);
|
msg.set_username(username);
|
||||||
msg.set_opus(true);
|
msg.set_opus(true);
|
||||||
outgoing.send(msg.into()).await.unwrap();
|
send_chan.send(msg.into()).await.unwrap();
|
||||||
|
|
||||||
// Spawn worker to send pings
|
// Spawn worker to send pings
|
||||||
{
|
{
|
||||||
let mut send_chan = outgoing.clone();
|
let mut send_chan = send_chan.clone();
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
if let Err(_) = send_chan.send(msgs::Ping::new().into()).await {
|
if let Err(_) = send_chan.send(msgs::Ping::new().into()).await {
|
||||||
@@ -121,11 +117,10 @@ pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut audio = AudioSystem::new().await?;
|
let mut audio = AudioSystem::new().await?;
|
||||||
audio.set_processor(AudioProcessor::new(audio_settings.denoise));
|
|
||||||
{
|
{
|
||||||
let send_chan = outgoing.clone();
|
let send_chan = send_chan.clone();
|
||||||
let mut sequence_num = 0;
|
let mut sequence_num = 0;
|
||||||
if let Err(err) = audio.start_recording(move |opus_frame, is_terminator| {
|
audio.start_recording(move |opus_frame, is_terminator| {
|
||||||
let _ =
|
let _ =
|
||||||
send_chan.unbounded_send(ControlPacket::UDPTunnel(Box::new(VoicePacket::Audio {
|
send_chan.unbounded_send(ControlPacket::UDPTunnel(Box::new(VoicePacket::Audio {
|
||||||
_dst: std::marker::PhantomData,
|
_dst: std::marker::PhantomData,
|
||||||
@@ -136,9 +131,7 @@ pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
|
|||||||
position_info: None,
|
position_info: None,
|
||||||
})));
|
})));
|
||||||
sequence_num = sequence_num.wrapping_add(2);
|
sequence_num = sequence_num.wrapping_add(2);
|
||||||
}) {
|
});
|
||||||
error!("could not begin recording: {err:?}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create map of session_id -> AudioDecoder
|
// Create map of session_id -> AudioDecoder
|
||||||
@@ -156,7 +149,7 @@ pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
|
|||||||
if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) {
|
if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) {
|
||||||
info!("receiving packet {:#?}", msg);
|
info!("receiving packet {:#?}", msg);
|
||||||
}
|
}
|
||||||
let res = accept_packet(msg, &mut audio, &mut decoder_map, &state);
|
let res = accept_packet(msg, &mut audio, &mut decoder_map);
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
error!("error accepting packet {:?}", err)
|
error!("error accepting packet {:?}", err)
|
||||||
}
|
}
|
||||||
@@ -175,7 +168,7 @@ pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
|
|||||||
match command {
|
match command {
|
||||||
Some(Command::Disconnect) => break,
|
Some(Command::Disconnect) => break,
|
||||||
Some(command) => {
|
Some(command) => {
|
||||||
let res = accept_command(command, &mut outgoing, &mut audio, &state);
|
let res = accept_command(command, &mut send_chan, &mut audio);
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
info!("error accepting command {:?}", err)
|
info!("error accepting command {:?}", err)
|
||||||
}
|
}
|
||||||
@@ -185,7 +178,7 @@ pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = outgoing.close();
|
let _ = send_chan.close();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -194,10 +187,9 @@ fn accept_command(
|
|||||||
command: Command,
|
command: Command,
|
||||||
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
|
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
|
||||||
audio: &mut AudioSystem,
|
audio: &mut AudioSystem,
|
||||||
state: &State,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
use Command::*;
|
use Command::*;
|
||||||
let Some(session) = state.server.read().session else {
|
let Some(session) = STATE.server.read().session else {
|
||||||
bail!("no session id")
|
bail!("no session id")
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,7 +212,7 @@ fn accept_command(
|
|||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
let Some(me) = server.session else {
|
let Some(me) = server.session else {
|
||||||
bail!("not signed in with a session id")
|
bail!("not signed in with a session id")
|
||||||
};
|
};
|
||||||
@@ -261,7 +253,7 @@ fn accept_command(
|
|||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
let Some(me) = server.session else {
|
let Some(me) = server.session else {
|
||||||
bail!("not signed in with a session id")
|
bail!("not signed in with a session id")
|
||||||
};
|
};
|
||||||
@@ -296,8 +288,12 @@ fn accept_command(
|
|||||||
let _ = send_chan.unbounded_send(u.into());
|
let _ = send_chan.unbounded_send(u.into());
|
||||||
}
|
}
|
||||||
Connect { .. } | Disconnect => (),
|
Connect { .. } | Disconnect => (),
|
||||||
UpdateAudioSettings(AudioSettings { denoise }) => {
|
UpdateMicEffects { denoise } => {
|
||||||
audio.set_processor(AudioProcessor::new(denoise));
|
if denoise {
|
||||||
|
audio.set_processor(AudioProcessor::new_denoising());
|
||||||
|
} else {
|
||||||
|
audio.set_processor(AudioProcessor::new_plain());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +304,6 @@ fn accept_packet(
|
|||||||
msg: ControlPacket<mumble_protocol::Clientbound>,
|
msg: ControlPacket<mumble_protocol::Clientbound>,
|
||||||
audio_context: &mut AudioSystem,
|
audio_context: &mut AudioSystem,
|
||||||
player_map: &mut HashMap<u32, AudioPlayer>,
|
player_map: &mut HashMap<u32, AudioPlayer>,
|
||||||
state: &State,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
match msg {
|
match msg {
|
||||||
ControlPacket::UDPTunnel(u) => {
|
ControlPacket::UDPTunnel(u) => {
|
||||||
@@ -345,15 +340,15 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::ChannelState(u) => {
|
ControlPacket::ChannelState(u) => {
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
server.channels_state.update_from_channel_state(&u);
|
server.channels_state.update_from_channel_state(&u);
|
||||||
}
|
}
|
||||||
ControlPacket::ChannelRemove(u) => {
|
ControlPacket::ChannelRemove(u) => {
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
server.channels_state.update_from_channel_remove(&u);
|
server.channels_state.update_from_channel_remove(&u);
|
||||||
}
|
}
|
||||||
ControlPacket::UserState(u) => {
|
ControlPacket::UserState(u) => {
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
let server = &mut *server;
|
let server = &mut *server;
|
||||||
let id = u.get_session();
|
let id = u.get_session();
|
||||||
|
|
||||||
@@ -397,7 +392,7 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::UserRemove(u) => {
|
ControlPacket::UserRemove(u) => {
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
let id = u.get_session();
|
let id = u.get_session();
|
||||||
if let Some(state) = server.users.remove(&id) {
|
if let Some(state) = server.users.remove(&id) {
|
||||||
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
||||||
@@ -406,7 +401,7 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::TextMessage(u) => {
|
ControlPacket::TextMessage(u) => {
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
if u.has_message() {
|
if u.has_message() {
|
||||||
let text = u.get_message().to_string();
|
let text = u.get_message().to_string();
|
||||||
server.chat.push(Chat {
|
server.chat.push(Chat {
|
||||||
@@ -421,8 +416,8 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::ServerSync(u) => {
|
ControlPacket::ServerSync(u) => {
|
||||||
*state.status.write_unchecked() = ConnectionState::Connected;
|
*STATE.status.write() = ConnectionState::Connected;
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
if u.has_welcome_text() {
|
if u.has_welcome_text() {
|
||||||
let text = u.get_welcome_text().to_string();
|
let text = u.get_welcome_text().to_string();
|
||||||
server.chat.push(Chat {
|
server.chat.push(Chat {
|
||||||
+2
-1176
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -12,7 +12,7 @@ tokio-rustls = "0.26"
|
|||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
tracing = { version = "^0.1.40", features = ["async-await"] }
|
tracing = { version = "^0.1.40", features = ["async-await"] }
|
||||||
tracing-subscriber = { version = "^0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "^0.3.18", features = ["env-filter"] }
|
||||||
mumble-web2-common = { workspace = true, features = ["networking"] }
|
mumble-web2-common = { workspace = true }
|
||||||
salvo = { version = "^0.84.2", features = [
|
salvo = { version = "^0.84.2", features = [
|
||||||
"quinn",
|
"quinn",
|
||||||
"eyre",
|
"eyre",
|
||||||
@@ -28,3 +28,4 @@ rcgen = "^0.13.2"
|
|||||||
hmac-sha256 = "^1.1.8"
|
hmac-sha256 = "^1.1.8"
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
url = { version = "2", features = ["serde"] }
|
url = { version = "2", features = ["serde"] }
|
||||||
|
rand = "0.9.2"
|
||||||
|
|||||||
+78
-16
@@ -1,5 +1,6 @@
|
|||||||
use color_eyre::eyre::{anyhow, bail, Context, Result};
|
use color_eyre::eyre::{anyhow, bail, Context, Result};
|
||||||
use mumble_web2_common::{ping_server, ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||||
|
use rand::Rng;
|
||||||
use salvo::conn::rustls::{Keycert, RustlsConfig};
|
use salvo::conn::rustls::{Keycert, RustlsConfig};
|
||||||
use salvo::cors::{AllowOrigin, Cors};
|
use salvo::cors::{AllowOrigin, Cors};
|
||||||
use salvo::logging::Logger;
|
use salvo::logging::Logger;
|
||||||
@@ -15,7 +16,7 @@ use tokio::net::TcpStream;
|
|||||||
use tokio::pin;
|
use tokio::pin;
|
||||||
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
||||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||||
use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct};
|
use tokio_rustls::rustls::{ClientConfig as RlsClientConfig, DigitallySignedStruct};
|
||||||
use tokio_rustls::{rustls, TlsConnector};
|
use tokio_rustls::{rustls, TlsConnector};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use tracing::info_span;
|
use tracing::info_span;
|
||||||
@@ -25,6 +26,8 @@ use tracing_subscriber::filter::LevelFilter;
|
|||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
mod ping;
|
||||||
|
|
||||||
fn default_cert_alt_names() -> Vec<String> {
|
fn default_cert_alt_names() -> Vec<String> {
|
||||||
vec!["localhost".into()]
|
vec!["localhost".into()]
|
||||||
}
|
}
|
||||||
@@ -74,7 +77,7 @@ async fn main() -> Result<()> {
|
|||||||
.install_default()
|
.install_default()
|
||||||
.map_err(|e| anyhow!("could not install crypto provider {e:?}"))?;
|
.map_err(|e| anyhow!("could not install crypto provider {e:?}"))?;
|
||||||
|
|
||||||
let mut overrides = ProxyOverrides {
|
let mut client_config = ClientConfig {
|
||||||
proxy_url: match &server_config.proxy_url {
|
proxy_url: match &server_config.proxy_url {
|
||||||
Some(url) => Some(url.to_string()),
|
Some(url) => Some(url.to_string()),
|
||||||
None => None,
|
None => None,
|
||||||
@@ -99,7 +102,7 @@ async fn main() -> Result<()> {
|
|||||||
let cert = cert_params.self_signed(&key_pair)?;
|
let cert = cert_params.self_signed(&key_pair)?;
|
||||||
|
|
||||||
let hash = hmac_sha256::Hash::hash(cert.der().as_ref());
|
let hash = hmac_sha256::Hash::hash(cert.der().as_ref());
|
||||||
overrides.cert_hash = Some(hash.into());
|
client_config.cert_hash = Some(hash.into());
|
||||||
|
|
||||||
(cert.pem().into(), key_pair.serialize_pem().into())
|
(cert.pem().into(), key_pair.serialize_pem().into())
|
||||||
}
|
}
|
||||||
@@ -119,11 +122,14 @@ async fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
let rustls_config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice()));
|
let rustls_config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice()));
|
||||||
|
|
||||||
info!("proxy overrides:\n{}", toml::to_string_pretty(&overrides)?);
|
info!(
|
||||||
|
"client config:\n{}",
|
||||||
|
toml::to_string_pretty(&client_config)?
|
||||||
|
);
|
||||||
|
|
||||||
let config_craft = ConfigCraft {
|
let config_craft = ConfigCraft {
|
||||||
server_config: server_config.clone(),
|
server_config: server_config.clone(),
|
||||||
overrides,
|
client_config,
|
||||||
};
|
};
|
||||||
|
|
||||||
let status_craft = StatusCraft {
|
let status_craft = StatusCraft {
|
||||||
@@ -133,7 +139,7 @@ async fn main() -> Result<()> {
|
|||||||
// Server routing
|
// Server routing
|
||||||
let mut router = Router::new()
|
let mut router = Router::new()
|
||||||
.push(Router::with_path("/proxy").goal(config_craft.connect_proxy()))
|
.push(Router::with_path("/proxy").goal(config_craft.connect_proxy()))
|
||||||
.push(Router::with_path("/overrides").get(config_craft.get_overrides()))
|
.push(Router::with_path("/config").get(config_craft.get_config()))
|
||||||
.push(Router::with_path("/status").get(status_craft.get_status()))
|
.push(Router::with_path("/status").get(status_craft.get_status()))
|
||||||
.hoop(Logger::new());
|
.hoop(Logger::new());
|
||||||
if let Some(gui_path) = server_config.gui_path.clone() {
|
if let Some(gui_path) = server_config.gui_path.clone() {
|
||||||
@@ -176,28 +182,84 @@ pub struct StatusCraft {
|
|||||||
impl StatusCraft {
|
impl StatusCraft {
|
||||||
#[craft(handler)]
|
#[craft(handler)]
|
||||||
async fn get_status(&self) -> Json<ServerStatus> {
|
async fn get_status(&self) -> Json<ServerStatus> {
|
||||||
let addr = self.mumble_server_address;
|
let mut server_status = ServerStatus::default();
|
||||||
match ping_server(&addr.ip().to_string(), addr.port()).await {
|
|
||||||
Ok(status) => Json(status),
|
let ping_packet = ping::PingPacket {
|
||||||
|
id: rand::rng().random(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sock = match tokio::net::UdpSocket::bind("0.0.0.0:0").await {
|
||||||
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("ping failed: {e:#}");
|
error!("Could not bind udp socket: {}", e);
|
||||||
Json(ServerStatus::default())
|
return Json(server_status);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match sock.connect(self.mumble_server_address).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Could not send ping packet: {}", e);
|
||||||
|
return Json(server_status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match sock.send(&<[u8; 12]>::from(ping_packet)).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Could not send ping packet");
|
||||||
|
return Json(server_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pong_buf: [u8; 24] = [0; 24];
|
||||||
|
|
||||||
|
match tokio::time::timeout(
|
||||||
|
tokio::time::Duration::from_secs(1),
|
||||||
|
sock.recv(&mut pong_buf),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Could not send ping packet");
|
||||||
|
return Json(server_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pong_packet = match ping::PongPacket::try_from(pong_buf.as_slice()) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Could not parse pong packet: {:?}", e);
|
||||||
|
return Json(server_status);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
server_status.success = true;
|
||||||
|
server_status.version = Some((
|
||||||
|
pong_packet.version & 0xFF,
|
||||||
|
(pong_packet.version >> 8) & 0xFF,
|
||||||
|
(pong_packet.version >> 16) & 0xFF,
|
||||||
|
));
|
||||||
|
server_status.users = Some(pong_packet.users);
|
||||||
|
server_status.max_users = Some(pong_packet.max_users);
|
||||||
|
server_status.bandwidth = Some(pong_packet.bandwidth);
|
||||||
|
|
||||||
|
Json(server_status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConfigCraft {
|
pub struct ConfigCraft {
|
||||||
server_config: Arc<Config>,
|
server_config: Arc<Config>,
|
||||||
overrides: ProxyOverrides,
|
client_config: ClientConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[craft]
|
#[craft]
|
||||||
impl ConfigCraft {
|
impl ConfigCraft {
|
||||||
#[craft(handler)]
|
#[craft(handler)]
|
||||||
async fn get_overrides(&self) -> Json<ProxyOverrides> {
|
async fn get_config(&self) -> Json<ClientConfig> {
|
||||||
Json(self.overrides.clone())
|
Json(self.client_config.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[craft(handler)]
|
#[craft(handler)]
|
||||||
@@ -258,7 +320,7 @@ async fn connect_proxy_impl(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!("connecting to Mumble server...");
|
info!("connecting to Mumble server...");
|
||||||
|
|
||||||
let config = ClientConfig::builder()
|
let config = RlsClientConfig::builder()
|
||||||
.dangerous()
|
.dangerous()
|
||||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||||
.with_no_client_auth();
|
.with_no_client_auth();
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// This code was taken from mumble-protocol-2x (https://github.com/dblsaiko/rust-mumble-protocol)
|
||||||
|
// and originally from mumble-protocol (https://github.com/Johni0702/rust-mumble-protocol)
|
||||||
|
// These projects are licensed under MIT and Apache 2.0.
|
||||||
|
|
||||||
|
//! Ping messages and codec
|
||||||
|
//!
|
||||||
|
//! A Mumble client can send periodic UDP [PingPacket]s to servers
|
||||||
|
//! in order to query their current state and measure latency.
|
||||||
|
//! A server will usually respond with a corresponding [PongPacket] containing
|
||||||
|
//! the requested details.
|
||||||
|
//!
|
||||||
|
//! Both packets are of fixed size and can be converted to/from `u8` arrays/slices via
|
||||||
|
//! the respective `From`/`TryFrom` impls.
|
||||||
|
|
||||||
|
/// A ping packet sent to the server.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct PingPacket {
|
||||||
|
/// Opaque, client-generated id.
|
||||||
|
///
|
||||||
|
/// Will be returned by the server unmodified and can be used to correlate
|
||||||
|
/// pong replies to ping requests to e.g. calculate latency.
|
||||||
|
pub id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pong packet sent to the client in reply to a previously received [PingPacket].
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct PongPacket {
|
||||||
|
/// Opaque, client-generated id.
|
||||||
|
///
|
||||||
|
/// Should match the value in the corresponding [PingPacket].
|
||||||
|
pub id: u64,
|
||||||
|
|
||||||
|
/// Server version. E.g. `0x010300` for `1.3.0`.
|
||||||
|
pub version: u32,
|
||||||
|
|
||||||
|
/// Current amount of users connected to the server.
|
||||||
|
pub users: u32,
|
||||||
|
|
||||||
|
/// Configured limit on the amount of users which can be connected to the server.
|
||||||
|
pub max_users: u32,
|
||||||
|
|
||||||
|
/// Maximum bandwidth for server-bound speech per client in bits per second
|
||||||
|
pub bandwidth: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error during parsing of a [PingPacket].
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum ParsePingError {
|
||||||
|
/// Ping packets must always be 12 bytes in size.
|
||||||
|
InvalidSize,
|
||||||
|
/// Ping packets must have an all zero header of 4 bytes.
|
||||||
|
InvalidHeader,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for PingPacket {
|
||||||
|
type Error = ParsePingError;
|
||||||
|
fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
match <[u8; 12]>::try_from(buf) {
|
||||||
|
Ok(array) => {
|
||||||
|
if array[0..4] != [0, 0, 0, 0] {
|
||||||
|
Err(ParsePingError::InvalidHeader)
|
||||||
|
} else {
|
||||||
|
Ok(Self {
|
||||||
|
id: u64::from_be_bytes(array[4..12].try_into().unwrap()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err(ParsePingError::InvalidSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PingPacket> for [u8; 12] {
|
||||||
|
fn from(packet: PingPacket) -> Self {
|
||||||
|
let id = packet.id.to_be_bytes();
|
||||||
|
// Is there no nicer way to do this?
|
||||||
|
[
|
||||||
|
0, 0, 0, 0, id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error during parsing of a [PongPacket].
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum ParsePongError {
|
||||||
|
/// Pong packets must always be 24 bytes in size.
|
||||||
|
InvalidSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for PongPacket {
|
||||||
|
type Error = ParsePongError;
|
||||||
|
fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
match <[u8; 24]>::try_from(buf) {
|
||||||
|
Ok(array) => Ok(Self {
|
||||||
|
version: u32::from_be_bytes(array[0..4].try_into().unwrap()),
|
||||||
|
id: u64::from_be_bytes(array[4..12].try_into().unwrap()),
|
||||||
|
users: u32::from_be_bytes(array[12..16].try_into().unwrap()),
|
||||||
|
max_users: u32::from_be_bytes(array[16..20].try_into().unwrap()),
|
||||||
|
bandwidth: u32::from_be_bytes(array[20..24].try_into().unwrap()),
|
||||||
|
}),
|
||||||
|
Err(_) => Err(ParsePongError::InvalidSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PongPacket> for [u8; 24] {
|
||||||
|
fn from(packet: PongPacket) -> Self {
|
||||||
|
let version = packet.version.to_be_bytes();
|
||||||
|
let id = packet.id.to_be_bytes();
|
||||||
|
let users = packet.users.to_be_bytes();
|
||||||
|
let max_users = packet.max_users.to_be_bytes();
|
||||||
|
let bandwidth = packet.bandwidth.to_be_bytes();
|
||||||
|
// Is there no nicer way to do this?
|
||||||
|
[
|
||||||
|
version[0],
|
||||||
|
version[1],
|
||||||
|
version[2],
|
||||||
|
version[3],
|
||||||
|
id[0],
|
||||||
|
id[1],
|
||||||
|
id[2],
|
||||||
|
id[3],
|
||||||
|
id[4],
|
||||||
|
id[5],
|
||||||
|
id[6],
|
||||||
|
id[7],
|
||||||
|
users[0],
|
||||||
|
users[1],
|
||||||
|
users[2],
|
||||||
|
users[3],
|
||||||
|
max_users[0],
|
||||||
|
max_users[1],
|
||||||
|
max_users[2],
|
||||||
|
max_users[3],
|
||||||
|
bandwidth[0],
|
||||||
|
bandwidth[1],
|
||||||
|
bandwidth[2],
|
||||||
|
bandwidth[3],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user