Upgrade Dioxus and auto-connect signaling
This commit is contained in:
parent
573abae5e3
commit
a917b4142b
2333
Cargo.lock
generated
2333
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
# Dioxus Framework
|
||||
dioxus = { version = "0.6.0", features = ["web"] }
|
||||
dioxus-logger = "0.6.2"
|
||||
dioxus = { version = "0.7", features = ["web"] }
|
||||
dioxus-logger = "0.7"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
# WebAssembly and Browser APIs
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"server": {
|
||||
"stun_server": "stun:stun.l.google.com:19302"
|
||||
"stun_server": "stun:stun.l.google.com:19302",
|
||||
"signaling_url": "ws://localhost:3478/ws"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
use crate::config::Config;
|
||||
use crate::models::SignalingMessage;
|
||||
use crate::utils::MediaManager;
|
||||
use dioxus::prelude::*;
|
||||
use futures::StreamExt;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::prelude::Closure;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
@ -19,9 +21,10 @@ pub fn ConnectionPanel(
|
||||
initiator_connection: Signal<Option<RtcPeerConnection>>, // Initiator PC (wird für eingehende Answers verwendet)
|
||||
local_media: Signal<Option<MediaStream>>,
|
||||
) -> Element {
|
||||
let mut ws_status = use_signal(|| "Nicht verbunden".to_string());
|
||||
let ws_status = use_signal(|| "Nicht verbunden".to_string());
|
||||
// Buffer for an incoming Answer SDP if the initiator PC isn't ready yet
|
||||
let mut pending_answer = use_signal(|| None::<String>);
|
||||
let pending_answer = use_signal(|| None::<String>);
|
||||
let cfg_signal: Signal<Config> = use_context();
|
||||
|
||||
// **COROUTINE** für Offer-Handling (Responder empfängt Offers)
|
||||
let offer_handler = use_coroutine(move |mut rx| async move {
|
||||
@ -174,11 +177,41 @@ pub fn ConnectionPanel(
|
||||
};
|
||||
|
||||
// WebSocket verbinden
|
||||
let connect_websocket = move |_| {
|
||||
log::info!("🔌 Verbinde WebSocket...");
|
||||
let connect_logic: Rc<dyn Fn()> = {
|
||||
let ws_status_signal = ws_status.clone();
|
||||
let connected_signal = connected.clone();
|
||||
let websocket_signal = websocket.clone();
|
||||
let offer_handler = offer_handler.clone();
|
||||
let pending_answer_handle = pending_answer.clone();
|
||||
let cfg_signal_handle = cfg_signal.clone();
|
||||
let initiator_connection_signal = initiator_connection.clone();
|
||||
let peer_connection_signal = peer_connection.clone();
|
||||
|
||||
Rc::new(move || {
|
||||
let mut ws_status = ws_status_signal.clone();
|
||||
let connected = connected_signal.clone();
|
||||
let mut websocket = websocket_signal.clone();
|
||||
let cfg_signal = cfg_signal_handle.clone();
|
||||
let initiator_connection = initiator_connection_signal.clone();
|
||||
let peer_connection = peer_connection_signal.clone();
|
||||
let pending_answer = pending_answer_handle.clone();
|
||||
|
||||
if *connected.read() || websocket.read().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
ws_status.set("Verbinde...".to_string());
|
||||
|
||||
match BrowserWebSocket::new("ws://localhost:3478/ws") {
|
||||
let endpoint = cfg_signal.read().server.signaling_url.trim().to_string();
|
||||
let target = if endpoint.is_empty() {
|
||||
crate::constants::DEFAULT_SIGNALING_URL.to_string()
|
||||
} else {
|
||||
endpoint
|
||||
};
|
||||
|
||||
log::info!("🔌 Verbinde WebSocket zu {}", target);
|
||||
|
||||
match BrowserWebSocket::new(&target) {
|
||||
Ok(socket) => {
|
||||
socket.set_binary_type(BinaryType::Arraybuffer);
|
||||
|
||||
@ -189,7 +222,8 @@ pub fn ConnectionPanel(
|
||||
log::info!("✅ WebSocket verbunden!");
|
||||
ws_status_clone.set("Verbunden".to_string());
|
||||
connected_clone.set(true);
|
||||
}) as Box<dyn FnMut(web_sys::Event)>);
|
||||
})
|
||||
as Box<dyn FnMut(web_sys::Event)>);
|
||||
|
||||
// onclose Handler
|
||||
let mut ws_status_clone2 = ws_status.clone();
|
||||
@ -203,6 +237,7 @@ pub fn ConnectionPanel(
|
||||
|
||||
// **MESSAGE ROUTER** - Leitet Messages an die richtigen Handler weiter
|
||||
let offer_tx = offer_handler.clone();
|
||||
let pending_answer_signal = pending_answer.clone();
|
||||
let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| {
|
||||
if let Some(text) = e.data().as_string() {
|
||||
log::info!("📨 WebSocket Nachricht: {}", text);
|
||||
@ -228,7 +263,9 @@ pub fn ConnectionPanel(
|
||||
} else {
|
||||
// Buffer the answer until an initiator PC exists
|
||||
log::warn!("⚠️ Keine Initiator-PeerConnection vorhanden - buffer Answer");
|
||||
pending_answer.set(Some(data_clone));
|
||||
let mut pending_answer_slot =
|
||||
pending_answer_signal.clone();
|
||||
pending_answer_slot.set(Some(data_clone));
|
||||
}
|
||||
}
|
||||
"candidate" => {
|
||||
@ -271,7 +308,8 @@ pub fn ConnectionPanel(
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut(MessageEvent)>);
|
||||
})
|
||||
as Box<dyn FnMut(MessageEvent)>);
|
||||
|
||||
socket.set_onopen(Some(onopen.as_ref().unchecked_ref()));
|
||||
socket.set_onclose(Some(onclose.as_ref().unchecked_ref()));
|
||||
@ -287,8 +325,16 @@ pub fn ConnectionPanel(
|
||||
log::error!("❌ WebSocket Fehler: {:?}", e);
|
||||
ws_status.set("Verbindungsfehler".to_string());
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
{
|
||||
let connect_logic = connect_logic.clone();
|
||||
use_effect(move || {
|
||||
connect_logic();
|
||||
});
|
||||
}
|
||||
|
||||
// Wenn eine gepufferte Answer vorhanden ist und später eine Initiator-PC gesetzt wird,
|
||||
// verarbeite die gepufferte Answer.
|
||||
@ -381,22 +427,6 @@ pub fn ConnectionPanel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section { class: "connection-card",
|
||||
header { class: "connection-card__header",
|
||||
h3 { "Networking" }
|
||||
}
|
||||
button {
|
||||
class: if *connected.read() { "btn btn--connected" } else { "btn" },
|
||||
disabled: *connected.read(),
|
||||
onclick: connect_websocket,
|
||||
if *connected.read() {
|
||||
"Connected"
|
||||
} else {
|
||||
"Connect"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,18 @@ use std::path::Path;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ServerOptions {
|
||||
#[serde(default = "default_stun_server")]
|
||||
pub stun_server: String,
|
||||
#[serde(default = "default_signaling_url")]
|
||||
pub signaling_url: String,
|
||||
}
|
||||
|
||||
fn default_stun_server() -> String {
|
||||
crate::constants::DEFAULT_STUN_SERVER.to_string()
|
||||
}
|
||||
|
||||
fn default_signaling_url() -> String {
|
||||
crate::constants::DEFAULT_SIGNALING_URL.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
@ -32,7 +43,9 @@ pub async fn load_config_from_server() -> Result<Config, Box<dyn std::error::Err
|
||||
}
|
||||
|
||||
// Fallback to fetching appsettings.json from the hosting origin
|
||||
let resp = gloo_net::http::Request::get("appsettings.json").send().await?;
|
||||
let resp = gloo_net::http::Request::get("appsettings.json")
|
||||
.send()
|
||||
.await?;
|
||||
let text = resp.text().await?;
|
||||
let cfg: Config = serde_json::from_str(&text)?;
|
||||
Ok(cfg)
|
||||
@ -41,7 +54,6 @@ pub async fn load_config_from_server() -> Result<Config, Box<dyn std::error::Err
|
||||
// Try to read a JSON config injected into index.html inside a script tag
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn load_config_from_html() -> Result<Option<Config>, Box<dyn std::error::Error>> {
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::window;
|
||||
|
||||
let win = window().ok_or("no window")?;
|
||||
@ -58,10 +70,9 @@ async fn load_config_from_html() -> Result<Option<Config>, Box<dyn std::error::E
|
||||
// Synchronous HTML fast-path for WASM: read script#app-config synchronously if present.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn load_config_from_html_sync() -> Option<Config> {
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::window;
|
||||
|
||||
let win = window().ok()?;
|
||||
let win = window()?;
|
||||
let doc = win.document()?;
|
||||
let elem = doc.get_element_by_id("app-config")?;
|
||||
if let Some(text) = elem.text_content() {
|
||||
@ -91,7 +102,12 @@ pub fn load_config_sync_or_default() -> Config {
|
||||
}
|
||||
|
||||
// Fallback default
|
||||
Config { server: ServerOptions { stun_server: crate::constants::DEFAULT_STUN_SERVER.to_string() } }
|
||||
Config {
|
||||
server: ServerOptions {
|
||||
stun_server: crate::constants::DEFAULT_STUN_SERVER.to_string(),
|
||||
signaling_url: crate::constants::DEFAULT_SIGNALING_URL.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Native loader convenience wrapper (blocking-friendly)
|
||||
@ -109,5 +125,10 @@ pub async fn load_config_or_default() -> Config {
|
||||
}
|
||||
|
||||
// Fallback default
|
||||
Config { server: ServerOptions { stun_server: crate::constants::DEFAULT_STUN_SERVER.to_string() } }
|
||||
Config {
|
||||
server: ServerOptions {
|
||||
stun_server: crate::constants::DEFAULT_STUN_SERVER.to_string(),
|
||||
signaling_url: crate::constants::DEFAULT_SIGNALING_URL.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// Central constants for niom-webrtc
|
||||
pub const DEFAULT_STUN_SERVER: &str = "stun:stun.l.google.com:19302";
|
||||
pub const DEFAULT_SIGNALING_URL: &str = "ws://localhost:3478/ws";
|
||||
pub const ASSET_FAVICON: &str = "/assets/favicon.ico";
|
||||
pub const ASSET_MAIN_CSS: &str = "/assets/main.css";
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
// Library root for niom-webrtc so integration tests and other crates can depend on the modules.
|
||||
pub mod components;
|
||||
pub mod models;
|
||||
pub mod utils;
|
||||
pub mod config;
|
||||
pub mod constants;
|
||||
pub mod models;
|
||||
pub mod utils;
|
||||
|
||||
// Re-export commonly used items if needed in the future
|
||||
// pub use config::*;
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
use crate::models::MediaState;
|
||||
use js_sys::Reflect;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{
|
||||
MediaStream, MediaStreamConstraints,
|
||||
RtcPeerConnection, RtcConfiguration, RtcIceServer,
|
||||
RtcSessionDescriptionInit, RtcSdpType,
|
||||
MediaStream, MediaStreamConstraints, RtcConfiguration, RtcIceServer, RtcPeerConnection,
|
||||
RtcSdpType, RtcSessionDescriptionInit,
|
||||
};
|
||||
use js_sys::Reflect;
|
||||
use crate::models::MediaState;
|
||||
|
||||
pub struct MediaManager {
|
||||
pub state: MediaState,
|
||||
@ -59,7 +58,10 @@ impl MediaManager {
|
||||
.map_err(|e| format!("set_local_description failed: {:?}", e))?;
|
||||
|
||||
log::info!("✅ Offer SDP length: {}", sdp.len());
|
||||
log::debug!("📋 SDP-Preview: {}...", &sdp[..std::cmp::min(150, sdp.len())]);
|
||||
log::debug!(
|
||||
"📋 SDP-Preview: {}...",
|
||||
&sdp[..std::cmp::min(150, sdp.len())]
|
||||
);
|
||||
|
||||
Ok(sdp)
|
||||
}
|
||||
@ -129,17 +131,18 @@ impl MediaManager {
|
||||
return Err("WebRTC not supported".into());
|
||||
}
|
||||
self.state = MediaState::Requesting;
|
||||
let navigator = web_sys::window()
|
||||
.ok_or("No window")?
|
||||
.navigator();
|
||||
let navigator = web_sys::window().ok_or("No window")?.navigator();
|
||||
let devices = navigator
|
||||
.media_devices()
|
||||
.map_err(|_| "MediaDevices not available")?;
|
||||
let constraints = MediaStreamConstraints::new();
|
||||
constraints.set_audio(&JsValue::from(true));
|
||||
constraints.set_video(&JsValue::from(false));
|
||||
let js_stream = JsFuture::from(devices.get_user_media_with_constraints(&constraints)
|
||||
.map_err(|e| format!("getUserMedia error: {:?}", e))?)
|
||||
let js_stream = JsFuture::from(
|
||||
devices
|
||||
.get_user_media_with_constraints(&constraints)
|
||||
.map_err(|e| format!("getUserMedia error: {:?}", e))?,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("getUserMedia promise rejected: {:?}", e))?;
|
||||
let stream: MediaStream = js_stream
|
||||
@ -156,9 +159,8 @@ impl MediaManager {
|
||||
// 1. JsValue holen
|
||||
let js_val = tracks.get(i);
|
||||
// 2. In MediaStreamTrack casten
|
||||
let track: web_sys::MediaStreamTrack = js_val
|
||||
.dyn_into()
|
||||
.expect("Expected MediaStreamTrack");
|
||||
let track: web_sys::MediaStreamTrack =
|
||||
js_val.dyn_into().expect("Expected MediaStreamTrack");
|
||||
// 3. Stoppen
|
||||
track.stop();
|
||||
log::info!("\u{1F6D1} Track gestoppt: {}", track.label());
|
||||
@ -184,9 +186,16 @@ impl MediaManager {
|
||||
// RtcRtpSender zurück; wir ignorieren den Rückgabewert hier.
|
||||
// `addTrack` ist in manchen web-sys-Versionen nicht direkt verfügbar.
|
||||
// Wir rufen die JS-Funktion dynamisch auf: pc.addTrack(track, stream)
|
||||
let add_fn = js_sys::Reflect::get(pc.as_ref(), &JsValue::from_str("addTrack")).map_err(|_| "Failed to get addTrack function".to_string())?;
|
||||
let func: js_sys::Function = add_fn.dyn_into().map_err(|_| "addTrack is not a function".to_string())?;
|
||||
let _ = func.call2(pc.as_ref(), &JsValue::from(track.clone()), &JsValue::from(stream.clone()));
|
||||
let add_fn = js_sys::Reflect::get(pc.as_ref(), &JsValue::from_str("addTrack"))
|
||||
.map_err(|_| "Failed to get addTrack function".to_string())?;
|
||||
let func: js_sys::Function = add_fn
|
||||
.dyn_into()
|
||||
.map_err(|_| "addTrack is not a function".to_string())?;
|
||||
let _ = func.call2(
|
||||
pc.as_ref(),
|
||||
&JsValue::from(track.clone()),
|
||||
&JsValue::from(stream.clone()),
|
||||
);
|
||||
log::info!("\u{2705} Track hinzugefügt: {}", track.label());
|
||||
}
|
||||
Ok(())
|
||||
@ -220,7 +229,9 @@ impl MediaManager {
|
||||
// Call pc.addIceCandidate(obj) dynamically
|
||||
let add_fn = js_sys::Reflect::get(pc.as_ref(), &JsValue::from_str("addIceCandidate"))
|
||||
.map_err(|_| "Failed to get addIceCandidate function".to_string())?;
|
||||
let func: js_sys::Function = add_fn.dyn_into().map_err(|_| "addIceCandidate is not a function".to_string())?;
|
||||
let func: js_sys::Function = add_fn
|
||||
.dyn_into()
|
||||
.map_err(|_| "addIceCandidate is not a function".to_string())?;
|
||||
let _ = func.call1(pc.as_ref(), &obj);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -6,7 +6,14 @@ fn sync_loader_returns_default_when_no_file() {
|
||||
// Ensure there's no appsettings.json in CWD for this test
|
||||
let _ = fs::remove_file("appsettings.json");
|
||||
let cfg = load_config_sync_or_default();
|
||||
assert_eq!(cfg.server.stun_server, niom_webrtc::constants::DEFAULT_STUN_SERVER.to_string());
|
||||
assert_eq!(
|
||||
cfg.server.stun_server,
|
||||
niom_webrtc::constants::DEFAULT_STUN_SERVER.to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.server.signaling_url,
|
||||
niom_webrtc::constants::DEFAULT_SIGNALING_URL.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
// This test ensures the function compiles and returns a Config; on native it will use the file-path,
|
||||
@ -16,4 +23,5 @@ fn sync_loader_api_callable() {
|
||||
let cfg = load_config_sync_or_default();
|
||||
// At minimum we have a non-empty stun_server
|
||||
assert!(!cfg.server.stun_server.is_empty());
|
||||
assert!(!cfg.server.signaling_url.is_empty());
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use niom_webrtc::constants::DEFAULT_STUN_SERVER;
|
||||
use niom_webrtc::config::Config;
|
||||
use niom_webrtc::constants::{DEFAULT_SIGNALING_URL, DEFAULT_STUN_SERVER};
|
||||
|
||||
#[test]
|
||||
fn default_stun_server_present() {
|
||||
@ -10,8 +10,16 @@ fn default_stun_server_present() {
|
||||
fn config_from_file_roundtrip() {
|
||||
// Create a temporary JSON in /tmp and read it via Config::from_file
|
||||
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
|
||||
let cfg_json = r#"{ "server": { "stun_server": "stun:example.org:3478" } }"#;
|
||||
let cfg_json = r#"{ "server": { "stun_server": "stun:example.org:3478", "signaling_url": "ws://example.org/ws" } }"#;
|
||||
std::fs::write(tmp.path(), cfg_json).expect("write");
|
||||
let cfg = Config::from_file(tmp.path()).expect("load");
|
||||
assert_eq!(cfg.server.stun_server, "stun:example.org:3478");
|
||||
assert_eq!(cfg.server.signaling_url, "ws://example.org/ws");
|
||||
|
||||
// Missing fields fall back to defaults
|
||||
let cfg_json_missing = r#"{ "server": { "stun_server": "stun:another.org:9999" } }"#;
|
||||
std::fs::write(tmp.path(), cfg_json_missing).expect("write missing");
|
||||
let cfg_missing = Config::from_file(tmp.path()).expect("load missing");
|
||||
assert_eq!(cfg_missing.server.stun_server, "stun:another.org:9999");
|
||||
assert_eq!(cfg_missing.server.signaling_url, DEFAULT_SIGNALING_URL);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user