Upgrade Dioxus and auto-connect signaling

This commit is contained in:
ghost 2025-11-02 14:32:12 +01:00
parent 573abae5e3
commit a917b4142b
10 changed files with 1735 additions and 996 deletions

2333
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,8 +6,8 @@ edition = "2021"
[dependencies] [dependencies]
# Dioxus Framework # Dioxus Framework
dioxus = { version = "0.6.0", features = ["web"] } dioxus = { version = "0.7", features = ["web"] }
dioxus-logger = "0.6.2" dioxus-logger = "0.7"
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
# WebAssembly and Browser APIs # WebAssembly and Browser APIs

View File

@ -1,5 +1,6 @@
{ {
"server": { "server": {
"stun_server": "stun:stun.l.google.com:19302" "stun_server": "stun:stun.l.google.com:19302",
"signaling_url": "ws://localhost:3478/ws"
} }
} }

View File

@ -1,7 +1,9 @@
use crate::config::Config;
use crate::models::SignalingMessage; use crate::models::SignalingMessage;
use crate::utils::MediaManager; use crate::utils::MediaManager;
use dioxus::prelude::*; use dioxus::prelude::*;
use futures::StreamExt; use futures::StreamExt;
use std::rc::Rc;
use wasm_bindgen::prelude::Closure; use wasm_bindgen::prelude::Closure;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
@ -19,9 +21,10 @@ pub fn ConnectionPanel(
initiator_connection: Signal<Option<RtcPeerConnection>>, // Initiator PC (wird für eingehende Answers verwendet) initiator_connection: Signal<Option<RtcPeerConnection>>, // Initiator PC (wird für eingehende Answers verwendet)
local_media: Signal<Option<MediaStream>>, local_media: Signal<Option<MediaStream>>,
) -> Element { ) -> 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 // 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) // **COROUTINE** für Offer-Handling (Responder empfängt Offers)
let offer_handler = use_coroutine(move |mut rx| async move { let offer_handler = use_coroutine(move |mut rx| async move {
@ -174,122 +177,165 @@ pub fn ConnectionPanel(
}; };
// WebSocket verbinden // WebSocket verbinden
let connect_websocket = move |_| { let connect_logic: Rc<dyn Fn()> = {
log::info!("🔌 Verbinde WebSocket..."); let ws_status_signal = ws_status.clone();
ws_status.set("Verbinde...".to_string()); 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();
match BrowserWebSocket::new("ws://localhost:3478/ws") { Rc::new(move || {
Ok(socket) => { let mut ws_status = ws_status_signal.clone();
socket.set_binary_type(BinaryType::Arraybuffer); 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();
// onopen Handler if *connected.read() || websocket.read().is_some() {
let mut ws_status_clone = ws_status.clone(); return;
let mut connected_clone = connected.clone(); }
let onopen = Closure::wrap(Box::new(move |_: web_sys::Event| {
log::info!("✅ WebSocket verbunden!");
ws_status_clone.set("Verbunden".to_string());
connected_clone.set(true);
}) as Box<dyn FnMut(web_sys::Event)>);
// onclose Handler ws_status.set("Verbinde...".to_string());
let mut ws_status_clone2 = ws_status.clone();
let mut connected_clone2 = connected.clone();
let onclose = Closure::wrap(Box::new(move |_: web_sys::CloseEvent| {
log::warn!("❌ WebSocket getrennt");
ws_status_clone2.set("Getrennt".to_string());
connected_clone2.set(false);
})
as Box<dyn FnMut(web_sys::CloseEvent)>);
// **MESSAGE ROUTER** - Leitet Messages an die richtigen Handler weiter let endpoint = cfg_signal.read().server.signaling_url.trim().to_string();
let offer_tx = offer_handler.clone(); let target = if endpoint.is_empty() {
let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| { crate::constants::DEFAULT_SIGNALING_URL.to_string()
if let Some(text) = e.data().as_string() { } else {
log::info!("📨 WebSocket Nachricht: {}", text); endpoint
};
if let Ok(msg) = serde_json::from_str::<SignalingMessage>(&text) { log::info!("🔌 Verbinde WebSocket zu {}", target);
match msg.msg_type.as_str() {
"offer" => { match BrowserWebSocket::new(&target) {
log::info!("🔀 Leite Offer an Responder-Handler weiter"); Ok(socket) => {
offer_tx.send(msg); socket.set_binary_type(BinaryType::Arraybuffer);
}
"answer" => { // onopen Handler
log::info!("🔀 Answer empfangen - leite an Initiator-PeerConnection weiter"); let mut ws_status_clone = ws_status.clone();
let data_clone = msg.data.clone(); let mut connected_clone = connected.clone();
if let Some(pc) = initiator_connection.read().as_ref() { let onopen = Closure::wrap(Box::new(move |_: web_sys::Event| {
// Versuche die Answer als Remote Description zu setzen log::info!("✅ WebSocket verbunden!");
let pc_clone = pc.clone(); ws_status_clone.set("Verbunden".to_string());
spawn_local(async move { connected_clone.set(true);
match crate::utils::MediaManager::handle_answer(&pc_clone, &data_clone).await { })
as Box<dyn FnMut(web_sys::Event)>);
// onclose Handler
let mut ws_status_clone2 = ws_status.clone();
let mut connected_clone2 = connected.clone();
let onclose = Closure::wrap(Box::new(move |_: web_sys::CloseEvent| {
log::warn!("❌ WebSocket getrennt");
ws_status_clone2.set("Getrennt".to_string());
connected_clone2.set(false);
})
as Box<dyn FnMut(web_sys::CloseEvent)>);
// **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);
if let Ok(msg) = serde_json::from_str::<SignalingMessage>(&text) {
match msg.msg_type.as_str() {
"offer" => {
log::info!("🔀 Leite Offer an Responder-Handler weiter");
offer_tx.send(msg);
}
"answer" => {
log::info!("🔀 Answer empfangen - leite an Initiator-PeerConnection weiter");
let data_clone = msg.data.clone();
if let Some(pc) = initiator_connection.read().as_ref() {
// Versuche die Answer als Remote Description zu setzen
let pc_clone = pc.clone();
spawn_local(async move {
match crate::utils::MediaManager::handle_answer(&pc_clone, &data_clone).await {
Ok(_) => log::info!("✅ Answer erfolgreich gesetzt auf Initiator-PC"), Ok(_) => log::info!("✅ Answer erfolgreich gesetzt auf Initiator-PC"),
Err(e) => log::error!("❌ Fehler beim Setzen der Answer auf Initiator-PC: {}", e), Err(e) => log::error!("❌ Fehler beim Setzen der Answer auf Initiator-PC: {}", e),
} }
}); });
} else { } else {
// Buffer the answer until an initiator PC exists // Buffer the answer until an initiator PC exists
log::warn!("⚠️ Keine Initiator-PeerConnection vorhanden - buffer Answer"); 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" => {
"candidate" => { log::info!("🔀 ICE-Kandidat empfangen: leite weiter");
log::info!("🔀 ICE-Kandidat empfangen: leite weiter"); // Determine whether this candidate is for initiator or responder
// Determine whether this candidate is for initiator or responder let data_clone = msg.data.clone();
let data_clone = msg.data.clone(); // Try initiator first
// Try initiator first if let Some(pc) = initiator_connection.read().as_ref() {
if let Some(pc) = initiator_connection.read().as_ref() { let pc_clone = pc.clone();
let pc_clone = pc.clone(); spawn_local(async move {
spawn_local(async move { match crate::utils::MediaManager::add_ice_candidate(&pc_clone, &data_clone) {
match crate::utils::MediaManager::add_ice_candidate(&pc_clone, &data_clone) { Ok(_) => log::info!("✅ Kandidat zur Initiator-PC hinzugefügt"),
Ok(_) => log::info!("✅ Kandidat zur Initiator-PC hinzugefügt"), Err(e) => log::error!("❌ Kandidat konnte nicht hinzugefügt werden: {}", e),
Err(e) => log::error!("❌ Kandidat konnte nicht hinzugefügt werden: {}", e), }
} });
}); } else if let Some(pc) = peer_connection.read().as_ref() {
} else if let Some(pc) = peer_connection.read().as_ref() { let pc_clone = pc.clone();
let pc_clone = pc.clone(); spawn_local(async move {
spawn_local(async move { match crate::utils::MediaManager::add_ice_candidate(&pc_clone, &data_clone) {
match crate::utils::MediaManager::add_ice_candidate(&pc_clone, &data_clone) { Ok(_) => log::info!("✅ Kandidat zur Responder-PC hinzugefügt"),
Ok(_) => log::info!("✅ Kandidat zur Responder-PC hinzugefügt"), Err(e) => log::error!("❌ Kandidat konnte nicht hinzugefügt werden: {}", e),
Err(e) => log::error!("❌ Kandidat konnte nicht hinzugefügt werden: {}", e), }
} });
}); } else {
} else { log::warn!("⚠️ Kein PeerConnection verfügbar, um Kandidaten hinzuzufügen");
log::warn!("⚠️ Kein PeerConnection verfügbar, um Kandidaten hinzuzufügen"); }
} }
} "text" => {
"text" => { log::info!("💬 Textnachricht: {}", msg.data);
log::info!("💬 Textnachricht: {}", msg.data); if let Some(window) = web_sys::window() {
if let Some(window) = web_sys::window() { let _ = window.alert_with_message(&format!(
let _ = window.alert_with_message(&format!( "Nachricht von {}:\n{}",
"Nachricht von {}:\n{}", msg.from, msg.data
msg.from, msg.data ));
)); }
}
_ => {
log::info!("❓ Unbekannte Nachricht: {}", msg.msg_type);
} }
}
_ => {
log::info!("❓ Unbekannte Nachricht: {}", msg.msg_type);
} }
} }
} }
} })
}) as Box<dyn FnMut(MessageEvent)>); as Box<dyn FnMut(MessageEvent)>);
socket.set_onopen(Some(onopen.as_ref().unchecked_ref())); socket.set_onopen(Some(onopen.as_ref().unchecked_ref()));
socket.set_onclose(Some(onclose.as_ref().unchecked_ref())); socket.set_onclose(Some(onclose.as_ref().unchecked_ref()));
socket.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); socket.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
onopen.forget(); onopen.forget();
onclose.forget(); onclose.forget();
onmessage.forget(); onmessage.forget();
websocket.set(Some(socket)); websocket.set(Some(socket));
} }
Err(e) => { Err(e) => {
log::error!("❌ WebSocket Fehler: {:?}", e); log::error!("❌ WebSocket Fehler: {:?}", e);
ws_status.set("Verbindungsfehler".to_string()); 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, // Wenn eine gepufferte Answer vorhanden ist und später eine Initiator-PC gesetzt wird,
// verarbeite die gepufferte Answer. // 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"
}
}
}
} }
} }
} }

View File

@ -3,7 +3,18 @@ use std::path::Path;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct ServerOptions { pub struct ServerOptions {
#[serde(default = "default_stun_server")]
pub stun_server: String, 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)] #[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 // 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 text = resp.text().await?;
let cfg: Config = serde_json::from_str(&text)?; let cfg: Config = serde_json::from_str(&text)?;
Ok(cfg) 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 // Try to read a JSON config injected into index.html inside a script tag
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
async fn load_config_from_html() -> Result<Option<Config>, Box<dyn std::error::Error>> { async fn load_config_from_html() -> Result<Option<Config>, Box<dyn std::error::Error>> {
use wasm_bindgen::JsCast;
use web_sys::window; use web_sys::window;
let win = window().ok_or("no 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. // Synchronous HTML fast-path for WASM: read script#app-config synchronously if present.
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn load_config_from_html_sync() -> Option<Config> { pub fn load_config_from_html_sync() -> Option<Config> {
use wasm_bindgen::JsCast;
use web_sys::window; use web_sys::window;
let win = window().ok()?; let win = window()?;
let doc = win.document()?; let doc = win.document()?;
let elem = doc.get_element_by_id("app-config")?; let elem = doc.get_element_by_id("app-config")?;
if let Some(text) = elem.text_content() { if let Some(text) = elem.text_content() {
@ -91,7 +102,12 @@ pub fn load_config_sync_or_default() -> Config {
} }
// Fallback default // 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) // Native loader convenience wrapper (blocking-friendly)
@ -109,5 +125,10 @@ pub async fn load_config_or_default() -> Config {
} }
// Fallback default // 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(),
},
}
} }

View File

@ -1,4 +1,5 @@
// Central constants for niom-webrtc // Central constants for niom-webrtc
pub const DEFAULT_STUN_SERVER: &str = "stun:stun.l.google.com:19302"; 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_FAVICON: &str = "/assets/favicon.ico";
pub const ASSET_MAIN_CSS: &str = "/assets/main.css"; pub const ASSET_MAIN_CSS: &str = "/assets/main.css";

View File

@ -1,9 +1,9 @@
// Library root for niom-webrtc so integration tests and other crates can depend on the modules. // Library root for niom-webrtc so integration tests and other crates can depend on the modules.
pub mod components; pub mod components;
pub mod models;
pub mod utils;
pub mod config; pub mod config;
pub mod constants; pub mod constants;
pub mod models;
pub mod utils;
// Re-export commonly used items if needed in the future // Re-export commonly used items if needed in the future
// pub use config::*; // pub use config::*;

View File

@ -1,13 +1,12 @@
use crate::models::MediaState;
use js_sys::Reflect;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture; use wasm_bindgen_futures::JsFuture;
use web_sys::{ use web_sys::{
MediaStream, MediaStreamConstraints, MediaStream, MediaStreamConstraints, RtcConfiguration, RtcIceServer, RtcPeerConnection,
RtcPeerConnection, RtcConfiguration, RtcIceServer, RtcSdpType, RtcSessionDescriptionInit,
RtcSessionDescriptionInit, RtcSdpType,
}; };
use js_sys::Reflect;
use crate::models::MediaState;
pub struct MediaManager { pub struct MediaManager {
pub state: MediaState, pub state: MediaState,
@ -21,10 +20,10 @@ impl MediaManager {
} }
pub fn create_peer_connection() -> Result<RtcPeerConnection, String> { pub fn create_peer_connection() -> Result<RtcPeerConnection, String> {
let ice_server = RtcIceServer::new(); let ice_server = RtcIceServer::new();
let urls = js_sys::Array::new(); let urls = js_sys::Array::new();
// Use centralized default STUN server constant // Use centralized default STUN server constant
urls.push(&JsValue::from_str(crate::constants::DEFAULT_STUN_SERVER)); urls.push(&JsValue::from_str(crate::constants::DEFAULT_STUN_SERVER));
ice_server.set_urls(&urls.into()); ice_server.set_urls(&urls.into());
let config = RtcConfiguration::new(); let config = RtcConfiguration::new();
let servers = js_sys::Array::new(); let servers = js_sys::Array::new();
@ -50,7 +49,7 @@ impl MediaManager {
.ok_or_else(|| "SDP field was not a string".to_string())?; .ok_or_else(|| "SDP field was not a string".to_string())?;
// 3. Init-Objekt bauen und SDP setzen // 3. Init-Objekt bauen und SDP setzen
let init = RtcSessionDescriptionInit::new(RtcSdpType::Offer); let init = RtcSessionDescriptionInit::new(RtcSdpType::Offer);
init.set_sdp(&sdp); init.set_sdp(&sdp);
// 4. Local Description setzen // 4. Local Description setzen
@ -59,7 +58,10 @@ impl MediaManager {
.map_err(|e| format!("set_local_description failed: {:?}", e))?; .map_err(|e| format!("set_local_description failed: {:?}", e))?;
log::info!("✅ Offer SDP length: {}", sdp.len()); 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) Ok(sdp)
} }
@ -96,7 +98,7 @@ impl MediaManager {
pub async fn handle_answer(pc: &RtcPeerConnection, answer_sdp: &str) -> Result<(), String> { pub async fn handle_answer(pc: &RtcPeerConnection, answer_sdp: &str) -> Result<(), String> {
log::info!("📨 Handling received answer..."); log::info!("📨 Handling received answer...");
// **DEBUG:** State vor Answer-Verarbeitung // **DEBUG:** State vor Answer-Verarbeitung
// Use the signaling_state() result for debug but avoid importing the enum type locally. // Use the signaling_state() result for debug but avoid importing the enum type locally.
let state = pc.signaling_state(); let state = pc.signaling_state();
@ -105,14 +107,14 @@ impl MediaManager {
if state != web_sys::RtcSignalingState::HaveLocalOffer { if state != web_sys::RtcSignalingState::HaveLocalOffer {
return Err(format!("❌ Falscher State für Answer: {:?}", state)); return Err(format!("❌ Falscher State für Answer: {:?}", state));
} }
let init = RtcSessionDescriptionInit::new(RtcSdpType::Answer); let init = RtcSessionDescriptionInit::new(RtcSdpType::Answer);
init.set_sdp(answer_sdp); init.set_sdp(answer_sdp);
JsFuture::from(pc.set_remote_description(&init)) JsFuture::from(pc.set_remote_description(&init))
.await .await
.map_err(|e| format!("set_remote_answer desc failed: {:?}", e))?; .map_err(|e| format!("set_remote_answer desc failed: {:?}", e))?;
log::info!("✅ Handled answer, WebRTC handshake complete!"); log::info!("✅ Handled answer, WebRTC handshake complete!");
Ok(()) Ok(())
} }
@ -129,19 +131,20 @@ impl MediaManager {
return Err("WebRTC not supported".into()); return Err("WebRTC not supported".into());
} }
self.state = MediaState::Requesting; self.state = MediaState::Requesting;
let navigator = web_sys::window() let navigator = web_sys::window().ok_or("No window")?.navigator();
.ok_or("No window")?
.navigator();
let devices = navigator let devices = navigator
.media_devices() .media_devices()
.map_err(|_| "MediaDevices not available")?; .map_err(|_| "MediaDevices not available")?;
let constraints = MediaStreamConstraints::new(); let constraints = MediaStreamConstraints::new();
constraints.set_audio(&JsValue::from(true)); constraints.set_audio(&JsValue::from(true));
constraints.set_video(&JsValue::from(false)); constraints.set_video(&JsValue::from(false));
let js_stream = JsFuture::from(devices.get_user_media_with_constraints(&constraints) let js_stream = JsFuture::from(
.map_err(|e| format!("getUserMedia error: {:?}", e))?) devices
.await .get_user_media_with_constraints(&constraints)
.map_err(|e| format!("getUserMedia promise rejected: {:?}", e))?; .map_err(|e| format!("getUserMedia error: {:?}", e))?,
)
.await
.map_err(|e| format!("getUserMedia promise rejected: {:?}", e))?;
let stream: MediaStream = js_stream let stream: MediaStream = js_stream
.dyn_into() .dyn_into()
.map_err(|_| "Failed to cast to MediaStream")?; .map_err(|_| "Failed to cast to MediaStream")?;
@ -156,9 +159,8 @@ impl MediaManager {
// 1. JsValue holen // 1. JsValue holen
let js_val = tracks.get(i); let js_val = tracks.get(i);
// 2. In MediaStreamTrack casten // 2. In MediaStreamTrack casten
let track: web_sys::MediaStreamTrack = js_val let track: web_sys::MediaStreamTrack =
.dyn_into() js_val.dyn_into().expect("Expected MediaStreamTrack");
.expect("Expected MediaStreamTrack");
// 3. Stoppen // 3. Stoppen
track.stop(); track.stop();
log::info!("\u{1F6D1} Track gestoppt: {}", track.label()); log::info!("\u{1F6D1} Track gestoppt: {}", track.label());
@ -184,9 +186,16 @@ impl MediaManager {
// RtcRtpSender zurück; wir ignorieren den Rückgabewert hier. // RtcRtpSender zurück; wir ignorieren den Rückgabewert hier.
// `addTrack` ist in manchen web-sys-Versionen nicht direkt verfügbar. // `addTrack` ist in manchen web-sys-Versionen nicht direkt verfügbar.
// Wir rufen die JS-Funktion dynamisch auf: pc.addTrack(track, stream) // 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 add_fn = js_sys::Reflect::get(pc.as_ref(), &JsValue::from_str("addTrack"))
let func: js_sys::Function = add_fn.dyn_into().map_err(|_| "addTrack is not a function".to_string())?; .map_err(|_| "Failed to get addTrack function".to_string())?;
let _ = func.call2(pc.as_ref(), &JsValue::from(track.clone()), &JsValue::from(stream.clone())); 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()); log::info!("\u{2705} Track hinzugefügt: {}", track.label());
} }
Ok(()) Ok(())
@ -220,7 +229,9 @@ impl MediaManager {
// Call pc.addIceCandidate(obj) dynamically // Call pc.addIceCandidate(obj) dynamically
let add_fn = js_sys::Reflect::get(pc.as_ref(), &JsValue::from_str("addIceCandidate")) let add_fn = js_sys::Reflect::get(pc.as_ref(), &JsValue::from_str("addIceCandidate"))
.map_err(|_| "Failed to get addIceCandidate function".to_string())?; .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); let _ = func.call1(pc.as_ref(), &obj);
Ok(()) Ok(())
} }

View File

@ -6,7 +6,14 @@ fn sync_loader_returns_default_when_no_file() {
// Ensure there's no appsettings.json in CWD for this test // Ensure there's no appsettings.json in CWD for this test
let _ = fs::remove_file("appsettings.json"); let _ = fs::remove_file("appsettings.json");
let cfg = load_config_sync_or_default(); 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, // 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(); let cfg = load_config_sync_or_default();
// At minimum we have a non-empty stun_server // At minimum we have a non-empty stun_server
assert!(!cfg.server.stun_server.is_empty()); assert!(!cfg.server.stun_server.is_empty());
assert!(!cfg.server.signaling_url.is_empty());
} }

View File

@ -1,5 +1,5 @@
use niom_webrtc::constants::DEFAULT_STUN_SERVER;
use niom_webrtc::config::Config; use niom_webrtc::config::Config;
use niom_webrtc::constants::{DEFAULT_SIGNALING_URL, DEFAULT_STUN_SERVER};
#[test] #[test]
fn default_stun_server_present() { fn default_stun_server_present() {
@ -10,8 +10,16 @@ fn default_stun_server_present() {
fn config_from_file_roundtrip() { fn config_from_file_roundtrip() {
// Create a temporary JSON in /tmp and read it via Config::from_file // Create a temporary JSON in /tmp and read it via Config::from_file
let tmp = tempfile::NamedTempFile::new().expect("tempfile"); 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"); std::fs::write(tmp.path(), cfg_json).expect("write");
let cfg = Config::from_file(tmp.path()).expect("load"); let cfg = Config::from_file(tmp.path()).expect("load");
assert_eq!(cfg.server.stun_server, "stun:example.org:3478"); 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);
} }