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,122 +177,165 @@ pub fn ConnectionPanel(
|
||||
};
|
||||
|
||||
// WebSocket verbinden
|
||||
let connect_websocket = move |_| {
|
||||
log::info!("🔌 Verbinde WebSocket...");
|
||||
ws_status.set("Verbinde...".to_string());
|
||||
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();
|
||||
|
||||
match BrowserWebSocket::new("ws://localhost:3478/ws") {
|
||||
Ok(socket) => {
|
||||
socket.set_binary_type(BinaryType::Arraybuffer);
|
||||
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();
|
||||
|
||||
// onopen Handler
|
||||
let mut ws_status_clone = ws_status.clone();
|
||||
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)>);
|
||||
if *connected.read() || websocket.read().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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)>);
|
||||
ws_status.set("Verbinde...".to_string());
|
||||
|
||||
// **MESSAGE ROUTER** - Leitet Messages an die richtigen Handler weiter
|
||||
let offer_tx = offer_handler.clone();
|
||||
let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| {
|
||||
if let Some(text) = e.data().as_string() {
|
||||
log::info!("📨 WebSocket Nachricht: {}", text);
|
||||
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
|
||||
};
|
||||
|
||||
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 {
|
||||
log::info!("🔌 Verbinde WebSocket zu {}", target);
|
||||
|
||||
match BrowserWebSocket::new(&target) {
|
||||
Ok(socket) => {
|
||||
socket.set_binary_type(BinaryType::Arraybuffer);
|
||||
|
||||
// onopen Handler
|
||||
let mut ws_status_clone = ws_status.clone();
|
||||
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
|
||||
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"),
|
||||
Err(e) => log::error!("❌ Fehler beim Setzen der Answer auf Initiator-PC: {}", e),
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Buffer the answer until an initiator PC exists
|
||||
log::warn!("⚠️ Keine Initiator-PeerConnection vorhanden - buffer Answer");
|
||||
pending_answer.set(Some(data_clone));
|
||||
});
|
||||
} else {
|
||||
// Buffer the answer until an initiator PC exists
|
||||
log::warn!("⚠️ Keine Initiator-PeerConnection vorhanden - buffer Answer");
|
||||
let mut pending_answer_slot =
|
||||
pending_answer_signal.clone();
|
||||
pending_answer_slot.set(Some(data_clone));
|
||||
}
|
||||
}
|
||||
}
|
||||
"candidate" => {
|
||||
log::info!("🔀 ICE-Kandidat empfangen: leite weiter");
|
||||
// Determine whether this candidate is for initiator or responder
|
||||
let data_clone = msg.data.clone();
|
||||
// Try initiator first
|
||||
if let Some(pc) = initiator_connection.read().as_ref() {
|
||||
let pc_clone = pc.clone();
|
||||
spawn_local(async move {
|
||||
match crate::utils::MediaManager::add_ice_candidate(&pc_clone, &data_clone) {
|
||||
Ok(_) => log::info!("✅ Kandidat zur Initiator-PC hinzugefügt"),
|
||||
Err(e) => log::error!("❌ Kandidat konnte nicht hinzugefügt werden: {}", e),
|
||||
}
|
||||
});
|
||||
} else if let Some(pc) = peer_connection.read().as_ref() {
|
||||
let pc_clone = pc.clone();
|
||||
spawn_local(async move {
|
||||
match crate::utils::MediaManager::add_ice_candidate(&pc_clone, &data_clone) {
|
||||
Ok(_) => log::info!("✅ Kandidat zur Responder-PC hinzugefügt"),
|
||||
Err(e) => log::error!("❌ Kandidat konnte nicht hinzugefügt werden: {}", e),
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log::warn!("⚠️ Kein PeerConnection verfügbar, um Kandidaten hinzuzufügen");
|
||||
"candidate" => {
|
||||
log::info!("🔀 ICE-Kandidat empfangen: leite weiter");
|
||||
// Determine whether this candidate is for initiator or responder
|
||||
let data_clone = msg.data.clone();
|
||||
// Try initiator first
|
||||
if let Some(pc) = initiator_connection.read().as_ref() {
|
||||
let pc_clone = pc.clone();
|
||||
spawn_local(async move {
|
||||
match crate::utils::MediaManager::add_ice_candidate(&pc_clone, &data_clone) {
|
||||
Ok(_) => log::info!("✅ Kandidat zur Initiator-PC hinzugefügt"),
|
||||
Err(e) => log::error!("❌ Kandidat konnte nicht hinzugefügt werden: {}", e),
|
||||
}
|
||||
});
|
||||
} else if let Some(pc) = peer_connection.read().as_ref() {
|
||||
let pc_clone = pc.clone();
|
||||
spawn_local(async move {
|
||||
match crate::utils::MediaManager::add_ice_candidate(&pc_clone, &data_clone) {
|
||||
Ok(_) => log::info!("✅ Kandidat zur Responder-PC hinzugefügt"),
|
||||
Err(e) => log::error!("❌ Kandidat konnte nicht hinzugefügt werden: {}", e),
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log::warn!("⚠️ Kein PeerConnection verfügbar, um Kandidaten hinzuzufügen");
|
||||
}
|
||||
}
|
||||
}
|
||||
"text" => {
|
||||
log::info!("💬 Textnachricht: {}", msg.data);
|
||||
if let Some(window) = web_sys::window() {
|
||||
let _ = window.alert_with_message(&format!(
|
||||
"Nachricht von {}:\n{}",
|
||||
msg.from, msg.data
|
||||
));
|
||||
"text" => {
|
||||
log::info!("💬 Textnachricht: {}", msg.data);
|
||||
if let Some(window) = web_sys::window() {
|
||||
let _ = window.alert_with_message(&format!(
|
||||
"Nachricht von {}:\n{}",
|
||||
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_onclose(Some(onclose.as_ref().unchecked_ref()));
|
||||
socket.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
|
||||
socket.set_onopen(Some(onopen.as_ref().unchecked_ref()));
|
||||
socket.set_onclose(Some(onclose.as_ref().unchecked_ref()));
|
||||
socket.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
|
||||
|
||||
onopen.forget();
|
||||
onclose.forget();
|
||||
onmessage.forget();
|
||||
onopen.forget();
|
||||
onclose.forget();
|
||||
onmessage.forget();
|
||||
|
||||
websocket.set(Some(socket));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("❌ WebSocket Fehler: {:?}", e);
|
||||
ws_status.set("Verbindungsfehler".to_string());
|
||||
}
|
||||
}
|
||||
websocket.set(Some(socket));
|
||||
}
|
||||
Err(e) => {
|
||||
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,
|
||||
@ -21,10 +20,10 @@ impl MediaManager {
|
||||
}
|
||||
|
||||
pub fn create_peer_connection() -> Result<RtcPeerConnection, String> {
|
||||
let ice_server = RtcIceServer::new();
|
||||
let urls = js_sys::Array::new();
|
||||
// Use centralized default STUN server constant
|
||||
urls.push(&JsValue::from_str(crate::constants::DEFAULT_STUN_SERVER));
|
||||
let ice_server = RtcIceServer::new();
|
||||
let urls = js_sys::Array::new();
|
||||
// Use centralized default STUN server constant
|
||||
urls.push(&JsValue::from_str(crate::constants::DEFAULT_STUN_SERVER));
|
||||
ice_server.set_urls(&urls.into());
|
||||
let config = RtcConfiguration::new();
|
||||
let servers = js_sys::Array::new();
|
||||
@ -50,7 +49,7 @@ impl MediaManager {
|
||||
.ok_or_else(|| "SDP field was not a string".to_string())?;
|
||||
|
||||
// 3. Init-Objekt bauen und SDP setzen
|
||||
let init = RtcSessionDescriptionInit::new(RtcSdpType::Offer);
|
||||
let init = RtcSessionDescriptionInit::new(RtcSdpType::Offer);
|
||||
init.set_sdp(&sdp);
|
||||
|
||||
// 4. Local Description setzen
|
||||
@ -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)
|
||||
}
|
||||
@ -96,7 +98,7 @@ impl MediaManager {
|
||||
|
||||
pub async fn handle_answer(pc: &RtcPeerConnection, answer_sdp: &str) -> Result<(), String> {
|
||||
log::info!("📨 Handling received answer...");
|
||||
|
||||
|
||||
// **DEBUG:** State vor Answer-Verarbeitung
|
||||
// Use the signaling_state() result for debug but avoid importing the enum type locally.
|
||||
let state = pc.signaling_state();
|
||||
@ -105,14 +107,14 @@ impl MediaManager {
|
||||
if state != web_sys::RtcSignalingState::HaveLocalOffer {
|
||||
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);
|
||||
|
||||
|
||||
JsFuture::from(pc.set_remote_description(&init))
|
||||
.await
|
||||
.map_err(|e| format!("set_remote_answer desc failed: {:?}", e))?;
|
||||
|
||||
|
||||
log::info!("✅ Handled answer, WebRTC handshake complete!");
|
||||
Ok(())
|
||||
}
|
||||
@ -129,19 +131,20 @@ 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))?)
|
||||
.await
|
||||
.map_err(|e| format!("getUserMedia promise rejected: {:?}", 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
|
||||
.dyn_into()
|
||||
.map_err(|_| "Failed to cast to MediaStream")?;
|
||||
@ -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