diff --git a/Cargo.toml b/Cargo.toml index 807cc76..1b005b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,14 +18,24 @@ js-sys = "0.3.61" # web-sys with features for media devices web-sys = { version = "0.3.77", features = [ "BinaryType", + "ErrorEvent", "Navigator", - "MediaDevices", + "MediaDevices", "MediaStream", "MediaStreamConstraints", "MediaStreamTrack", "MediaTrackSettings", "MediaTrackConstraints", - "AudioContext" + "AudioContext", + "RtcPeerConnection", + "RtcConfiguration", + "RtcIceServer", + "RtcIceCandidate", + "RtcIceCandidateInit", + "RtcSessionDescription", + "RtcSessionDescriptionInit", + "RtcOfferOptions", + "RtcAnswerOptions" ]} # Logging and Tracing diff --git a/src/components/call_controls.rs b/src/components/call_controls.rs new file mode 100644 index 0000000..ea42ffd --- /dev/null +++ b/src/components/call_controls.rs @@ -0,0 +1,141 @@ +use dioxus::prelude::*; +use web_sys::WebSocket as BrowserWebSocket; + +use crate::models::SignalingMessage; +use crate::utils::{MediaManager, MediaState}; + +#[component] +pub fn CallControls( + connected: Signal, + audio_enabled: Signal, + media_manager: Signal, + web_socket: Signal>, + local_peer_id: Signal, + remote_peer_id: Signal, +) -> Element { + let is_connected = move || connected + .try_read() + .map(|c| *c) + .unwrap_or(false); + + let is_audio_enabled = move || audio_enabled + .try_read() + .map(|a| *a) + .unwrap_or(true); + + let get_local_id = move || local_peer_id + .try_read() + .map(|id| id.clone()) + .unwrap_or_default(); + + let get_remote_id = move || remote_peer_id + .try_read() + .map(|id| id.clone()) + .unwrap_or_default(); + + let has_mic_permission = move || media_manager + .try_read() + .map(|m| m.is_microphone_active()) + .unwrap_or(false); + + let get_ws = move || web_socket + .try_read() + .ok() + .and_then(|ws_opt| ws_opt.as_ref().cloned()); + + rsx! { + div { class: "call-controls", + h2 { "Anruf-Steuerung" } + + // Mikrofon-Berechtigung Sektion + div { class: "mic-permission-section", + h3 { "Mikrofon-Berechtigung" } + button { + class: "mic-permission-btn primary", + disabled: has_mic_permission(), + onclick: move |_| { + spawn(async move { + if let Ok(mut manager) = media_manager.try_write() { + match manager.request_microphone_access().await { + Ok(_) => log::info!("Mikrofon-Berechtigung erteilt"), + Err(e) => log::error!("Berechtigung verweigert: {}", e), + } + } + }); + }, + if has_mic_permission() { + "✅ Berechtigung erteilt" + } else { + "🎤 Berechtigung erteilen" + } + } + } + + div { class: "control-buttons", + button { + class: "call-btn primary", + disabled: !is_connected() || !has_mic_permission() || get_remote_id().is_empty(), + onclick: move |_| { + let socket = get_ws(); + let local_id = get_local_id(); + let remote_id = get_remote_id(); + + spawn(async move { + match MediaManager::create_peer_connection() { + Ok(pc) => { + log::info!("PeerConnection erstellt, sende Offer..."); + + let offer_msg = SignalingMessage { + from: local_id.clone(), + to: remote_id.clone(), + msg_type: "offer".to_string(), + data: "dummy-sdp-offer".to_string(), + }; + + if let Some(ws) = socket { + if let Ok(json) = serde_json::to_string(&offer_msg) { + let _ = ws.send_with_str(&json); + log::info!("Offer gesendet an {}", remote_id); + } + } + } + Err(e) => log::error!("PeerConnection-Fehler: {}", e), + } + }); + }, + "📞 Anruf starten" + } + + // Audio-Stummschaltung + button { + class: if is_audio_enabled() { "mute-btn" } else { "mute-btn muted" }, + disabled: !is_connected() || !has_mic_permission(), + onclick: move |_| { + if let Ok(mut enabled) = audio_enabled.try_write() { + *enabled = !*enabled; + log::info!("Audio {}", if *enabled { "aktiviert" } else { "stumm" }); + } + }, + if is_audio_enabled() { + "🔊 Mikrofon an" + } else { + "🔇 Stumm geschaltet" + } + } + + // Anruf beenden + button { + class: "end-btn danger", + disabled: !is_connected(), + onclick: move |_| { + if let Ok(mut manager) = media_manager.try_write() { + manager.stop_stream(); + log::info!("Anruf beendet"); + } + }, + "📵 Anruf beenden" + } + } + } + } +} diff --git a/src/components/connection_panel.rs b/src/components/connection_panel.rs new file mode 100644 index 0000000..7b775c2 --- /dev/null +++ b/src/components/connection_panel.rs @@ -0,0 +1,76 @@ +use dioxus::prelude::*; +use web_sys::WebSocket as BrowserWebSocket; + +#[component] +pub fn ConnectionPanel( + connected: Signal, + local_peer_id: Signal, + remote_peer_id: Signal, + web_socket: Signal>, +) -> Element { + let is_connected = move || connected + .try_read() + .map(|c| *c) + .unwrap_or(false); + + let get_local_id = move || local_peer_id + .try_read() + .map(|id| id.clone()) + .unwrap_or_default(); + + let get_remote_id = move || remote_peer_id + .try_read() + .map(|id| id.clone()) + .unwrap_or_default(); + + rsx! { + div { class: "connection-panel", + h2 { "Verbindung" } + + div { class: "input-group", + label { r#for: "local-peer-id", "Ihre Peer ID:" } + input { + id: "local-peer-id", + class: "readonly-input", + r#type: "text", + value: "{get_local_id()}", + readonly: true + } + button { + class: "copy-btn", + onclick: move |_| log::info!("Peer-ID kopiert: {}", get_local_id()), + "📋" + } + } + + div { class: "input-group", + label { r#for: "remote-peer-id", "Remote Peer-ID:" } + input { + id: "remote-peer-id", + r#type: "text", + placeholder: "ID des anderen Teilnehmers eingeben", + value: "{get_remote_id()}", + oninput: move |event| { + if let Ok(mut remote) = remote_peer_id.try_write() { + *remote = event.value(); + } + } + } + } + + // **GEÄNDERT:** Button-Status basiert jetzt auf WebSocket-Verbindung + button { + class: "connect-btn", + disabled: is_connected(), + onclick: move |_| { + // Verbindung läuft automatisch + }, + if is_connected() { + "✅ Verbunden" + } else { + "🔄 Verbinde..." + } + } + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..8e75f1b --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,7 @@ +mod connection_panel; +mod call_controls; +mod status_display; + +pub use connection_panel::ConnectionPanel; +pub use call_controls::CallControls; +pub use status_display::StatusDisplay; diff --git a/src/components/status_display.rs b/src/components/status_display.rs new file mode 100644 index 0000000..98adf41 --- /dev/null +++ b/src/components/status_display.rs @@ -0,0 +1,131 @@ +use dioxus::prelude::*; +use crate::utils::{MediaManager, MediaState}; + +#[component] +pub fn StatusDisplay( + connected: Signal, + audio_enabled: Signal, + local_peer_id: Signal, + remote_peer_id: Signal, + media_manager: Signal, +) -> Element { + // **VOLLSTÄNDIG DEFENSIVE:** Alle Werte werden im use_effect gesetzt + let mut display_connected = use_signal(|| false); + let mut display_audio = use_signal(|| true); + let mut display_local_id = use_signal(|| String::from("Wird generiert...")); + let mut display_remote_id = use_signal(|| String::new()); + let mut display_mic_status = use_signal(|| String::from("Initialisierung...")); + let mut display_mic_class = use_signal(|| String::from("status-value")); + + // Sichere Signal-Updates in einem einzigen Effect + use_effect(move || { + // WebSocket-Status + if let Ok(conn) = connected.try_read() { + display_connected.set(*conn); + } + + // Audio-Status + if let Ok(audio) = audio_enabled.try_read() { + display_audio.set(*audio); + } + + // Local Peer-ID + if let Ok(local_id) = local_peer_id.try_read() { + if !local_id.is_empty() { + display_local_id.set(local_id.clone()); + } + } + + // Remote Peer-ID + if let Ok(remote_id) = remote_peer_id.try_read() { + display_remote_id.set(remote_id.clone()); + } + + // Media Manager Status + if let Ok(manager) = media_manager.try_read() { + let (status_text, status_class) = match &manager.state { + MediaState::Granted(_) => ("Erteilt", "status-value connected"), + MediaState::Denied(_) => ("Verweigert", "status-value disconnected"), + MediaState::Requesting => ("Angefragt...", "status-value requesting"), + MediaState::NotSupported => ("Nicht unterstützt", "status-value disconnected"), + MediaState::Uninitialized => ("Nicht initialisiert", "status-value"), + }; + display_mic_status.set(status_text.to_string()); + display_mic_class.set(status_class.to_string()); + } + }); + + rsx! { + div { class: "status-display", + h2 { "Status" } + + // **SICHER:** Nur lokale Signale verwenden + div { class: "status-item", + span { class: "status-label", "Signaling-Verbindung:" } + span { + class: if *display_connected.read() { + "status-value connected" + } else { + "status-value disconnected" + }, + if *display_connected.read() { + "Bereit für Anrufe" + } else { + "Verbinde mit Server..." + } + } + } + + // WebRTC-Verbindung + div { class: "status-item", + span { class: "status-label", "WebRTC-Verbindung:" } + span { class: "status-value disconnected", "Nicht verbunden" } + } + + // Mikrofon-Berechtigung + div { class: "status-item", + span { class: "status-label", "Mikrofon-Berechtigung:" } + span { + class: "{display_mic_class.read()}", + "{display_mic_status.read()}" + } + } + + // Audio-Status (nur bei Verbindung) + if *display_connected.read() { + div { class: "status-item", + span { class: "status-label", "Audio im Anruf:" } + span { + class: if *display_audio.read() { + "status-value connected" + } else { + "status-value disconnected" + }, + if *display_audio.read() { "Aktiv" } else { "Stumm geschaltet" } + } + } + } + + // Peer-IDs + div { class: "status-item", + span { class: "status-label", "Ihre ID:" } + span { class: "status-value peer-id", "{display_local_id.read()}" } + } + + // Remote Peer-ID (nur anzeigen wenn nicht leer) + if !display_remote_id.read().is_empty() { + div { class: "status-item", + span { class: "status-label", "Verbunden mit:" } + span { class: "status-value peer-id", "{display_remote_id.read()}" } + } + } + + // WebRTC-Support Warnung + if !MediaManager::is_webrtc_supported() { + div { class: "warning-message", + "⚠️ WebRTC wird von diesem Browser nicht unterstützt" + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs index ed0e0f1..700bedc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,25 @@ #![allow(non_snake_case)] +mod components; +mod models; mod utils; -use dioxus::{html::{g::media, h3}, prelude::*}; +use dioxus::prelude::*; use wasm_bindgen::JsCast; -use wasm_bindgen::prelude::Closure; +use wasm_bindgen::prelude::{Closure, JsValue}; use web_sys::{BinaryType, MessageEvent, WebSocket as BrowserWebSocket}; +use components::{ConnectionPanel, CallControls, StatusDisplay}; use utils::{MediaManager, MediaState}; +use models::SignalingMessage; const FAVICON: Asset = asset!("/assets/favicon.ico"); const MAIN_CSS: Asset = asset!("/assets/main.css"); -const HEADER_SVG: Asset = asset!("/assets/header.svg"); fn main() { + // Initialize logging + dioxus_logger::init(dioxus_logger::tracing::Level::INFO).expect("Failed to initialize logger"); + console_error_panic_hook::set_once(); dioxus::launch(App); } @@ -28,368 +34,151 @@ fn App() -> Element { #[component] pub fn Content() ->Element { - // State for connection status and audio - let mut connected = use_signal(|| false); - let mut audio_enabled = use_signal(|| true); - - // State for Peer IDs - let mut local_peer_id = use_signal(|| generate_peer_id()); - let mut remote_peer_id = use_signal(|| String::new()); - - let mut media_manager = use_signal(|| MediaManager::new()); + // Initialize signals + let connected = use_signal(|| false); + let audio_enabled = use_signal(|| true); + let local_peer_id = use_signal(|| generate_peer_id()); + let remote_peer_id = use_signal(|| String::new()); + let media_manager = use_signal(|| MediaManager::new()); + let web_socket= use_signal(|| None::); // On mount: Request microphone access if not already granted use_effect(move || { to_owned![media_manager]; spawn(async move { - match media_manager.write().request_microphone_access().await { - Ok(_) => log::info!("Microphone access granted"), - Err(e) => log::error!("Failed to request microphone access: {}", e) + if let Ok(mut manager) = media_manager.try_write() { + match manager.request_microphone_access().await { + Ok(_) => log::info!("Microphone access granted"), + Err(e) => log::error!("Failed to request microphone access: {}", e) } + } }); }); + // On Mount: Initialize WebSocket connection + use_effect(move || { + to_owned![web_socket, connected, local_peer_id, remote_peer_id]; + if web_socket.try_read().map(|w| w.is_none()).unwrap_or(true) { + match BrowserWebSocket::new("ws://localhost:3478/ws") { + Ok(socket) => { + socket.set_binary_type(BinaryType::Arraybuffer); + + // Event Handlers + let onerror = + Closure::wrap(Box::new(move |e: web_sys::ErrorEvent| { + log::error!("WebSocket-Fehler: {:?}", e); + }) as Box); + + // onclose Handler + let onclose = { + to_owned![connected]; + Closure::wrap(Box::new(move |_: web_sys::CloseEvent| { + log::warn!("WebSocket-Verbindung geschlossen"); + if let Ok(mut conn) = connected.try_write() { + *conn = false; + } + }) as Box) + }; + + let onmessage = { + to_owned![local_peer_id, remote_peer_id]; + Closure::wrap(Box::new(move |e: MessageEvent| { + if let Some(text) = e.data().as_string() { + log::info!("Websocket-Nachricht empfangen: {}", text); + + if let Ok(msg) = serde_json::from_str::(&text) { + match msg.msg_type.as_str() { + "offer" | "answer" | "ice-candidate" => { + log::info!("WebRTC-Nachricht empfangen: {}", msg.msg_type); + // TODO: WebRTC-Handler implementieren + } + "text" => { + log::info!("Text-Nachricht von {}: {}", msg.from, msg.data); + } + _ => { + log::warn!("Unbekannter Nachrichtentyp: {}", msg.msg_type); + } + } + } + } + }) as Box) + }; + + let onopen = { + to_owned![connected]; + Closure::wrap(Box::new(move |_: web_sys::Event| { + log::info!("WebSocket connected"); + if let Ok(mut conn) = connected.try_write() { + *conn = true; + } + }) as Box) + }; + + let onclose = { + to_owned![connected]; + Closure::wrap(Box::new(move |_: web_sys::CloseEvent| { + log::info!("WebSocket disconnected"); + if let Ok(mut conn) = connected.try_write() { + *conn = false; + } + }) as Box) + }; + + socket.set_onerror(Some(onerror.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())); + + onerror.forget(); + onmessage.forget(); + onopen.forget(); + onclose.forget(); + + if let Ok(mut ws) = web_socket.try_write() { + *ws = Some(socket); + } + } + Err(e) => + log::error!("Failed to create WebSocket: {:?}", e), + } + } + }); + rsx! { div { class: "app-container", - - // Header header { h1 { "Voice Chat MVP" } p { "WebRTC-basierter Sprachchat mit Ende-zu-Ende-Verschlüsselung" } } - main { class: "main-content", // Connection Panel ConnectionPanel { - connected: connected.clone(), - local_peer_id: local_peer_id.clone(), - remote_peer_id: remote_peer_id.clone() + connected, + local_peer_id, + remote_peer_id, + web_socket } // Call Controls CallControls { - connected: connected.clone(), - audio_enabled: audio_enabled.clone(), - media_manager: media_manager.clone(), - on_start_call: move |_| { - log::info!("Anruf wird gestartet mit Remote Peer: {}", remote_peer_id()); - }, - on_end_call: move |_| { - log::info!("Anruf wird beendet"); - connected.set(false); - media_manager.write().stop_stream(); - }, - on_toggle_audio: move |_| { - audio_enabled.set(!audio_enabled()); - log::info!("Audio Status: {}", if audio_enabled() { "Aktiviert" } else { "Deaktiviert" }); - } + connected, + audio_enabled, + media_manager, + web_socket, + local_peer_id, + remote_peer_id } // Status Display StatusDisplay { - connected: connected.clone(), - audio_enabled: audio_enabled.clone(), - local_peer_id: local_peer_id.clone(), - remote_peer_id: remote_peer_id.clone(), - media_manager: media_manager.clone() - } - } - } - } -} - -// Komponente für Verbindungseinstellungen -#[component] -fn ConnectionPanel( - connected: Signal, - local_peer_id: Signal, - remote_peer_id: Signal -) -> Element { - // Websocket signal - let ws = use_signal(|| None::); - - // One-time initialization of WebSocket connection - use_effect(move || { - to_owned![ws]; - - if ws.read().is_none() { - // Create new WebSocket connection - if let Ok(socket) = BrowserWebSocket::new("ws://localhost:8080/ws") { - socket.set_binary_type(BinaryType::Arraybuffer); - ws.write().replace(socket.clone()); - - // Event handler - let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| { - if let Some(text) = e.data().as_string() { - // Hier später SPD/Candidate verarbeiten - log::info!("WS empfangen: {}", text); - } - }) as Box); - - socket.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); - onmessage.forget(); // Verhindert, dass Closure gelöscht wird - } - } - }); - - rsx! { - div { - class: "connection-panel", - h2 { "Verbindung" } - - div { - class: "input-group", - label { r#for: "local-peer-id", "Ihre Peer ID:" } - input { - id: "local-peer-id", - class: "readonly-input", - r#type: "text", - value: "{local_peer_id()}", - readonly: true - } - button { - class:"copy-btn", - onclick: move |_| { - // Später: Implementierung für Copy-to-Clipboard - log::info!("Peer-ID kopiert: {}", local_peer_id()); - }, - "📋" - } - } - - div { - class: "input-group", - label { r#for: "remote-peer-id", "Remote Peer-ID:" } - input { - id: "remote-peer-id", - r#type: "text", - placeholder: "ID des anderen Teilnehmers eingeben", - value: "{remote_peer_id()}", - oninput: move |event| { - remote_peer_id.set(event.value()); - log::info!("Remote Peer-ID eingegeben: {}", event.value()); - } - } - } - - button { - class: "connect-btn", - disabled: ws.read().is_some(), - onclick: move |_| { - connected.set(true); - }, - if ws.read().is_some() { - "✅ Verbunden" - } - else { - "Mit Signaling Server verbinden" - } - } - } - } -} - -// Komponente für Anruf-Steuerungen -#[component] -fn CallControls( - connected: Signal, - audio_enabled: Signal, - mut media_manager: Signal, - on_start_call: EventHandler, - on_end_call: EventHandler, - on_toggle_audio: EventHandler -) -> Element { - rsx! { - div { - class: "call-controls", - h2 { "Anruf-Steuerung" } - - div { - class: "control-buttons", - - // Start Call - button { - class: "call-btn primary", - onclick: on_start_call, - disabled: !connected(), - "📞 Anruf starten" - } - - // Toggle Audio - button { - class: if audio_enabled() { - "mute-btn" - } - else { - "mute-btn-muted" - }, - onclick: on_toggle_audio, - disabled: !connected(), - if audio_enabled() { - "🎤 Mikrofon" - } - else { - "🔇 Stumm" - } - } - - // End Call - button { - class: "end-btn danger", - onclick: on_end_call, - disabled: !connected(), - "📵 Anruf beenden" - } - } - } - } -} - -// Komponente für Status-Anzeige -#[component] -fn StatusDisplay( - connected: Signal, - audio_enabled: Signal, - local_peer_id: Signal, - remote_peer_id: Signal, - media_manager: Signal -) -> Element { - rsx! { - div { - class: "status-display", - h2 { "Status" } - - div { - class: "status-item", - span { - class: "status-label", - "Mikrofon Berechtigung:" - } - span { - class: match &media_manager.read().state { - MediaState::Granted(_) => "status-value connected", - MediaState::Denied(_) => "status-value disconnected", - MediaState::Requesting => "status-value requesting", - _ => "status-value unknown" - }, - match &media_manager.read().state { - MediaState::Granted(_) => "Erteilt", - MediaState::Denied(_) => "Verweigert", - MediaState::Requesting => "Anfrage läuft", - _ => "Nicht verfügbar" - } - } - } - - div { - class: "status-item", - span { - class: "status-label", - "Mikrofon:" - } - span { - class: - if audio_enabled() { - "status-value connected" - } - else { - "status-value disconnected" - }, - if audio_enabled() { - "Entmuted" - } - else { - "Stumm" - } - } - } - - div { - class: "status-item", - span { - class: "status-label", - "Signaling Server:" - } - span { - class: - if connected() { - "status-value connected" - } - else { - "status-value disconnected" - }, - - if connected() { - "Verbunden" - } - else { - "Getrennt" - } - } - } - - div { - class: "status-item", - span { - class: "status-label", - "WebRTC Verbindung:" - } - span { - class: "status-value disconnected", - "Nicht verbunden" - } - } - - div { - class:"status-item", - span { - class: "status-label", - "Mikrofon Status:" - } - span { - class: match &media_manager.read().state { - MediaState::Granted(_) => "status-value connected", - MediaState::Denied(_) => "status-value disconnected", - MediaState::Requesting => "status-value requesting", - _ => "status-value" - }, - "{media_manager.read().get_status_text()}" - } - } - - div { - class: "status-item", - span { - class: "status-label", - "Lokale Peer-ID:" - } - span { - class: "status-value peer-id", - "{local_peer_id()}" - } - } - - if !remote_peer_id().is_empty() { - div { - class: "status-item", - span { - class: "status-label", - "Remote Peer-ID:" - } - span { - class: "status-value peer-id", - "{remote_peer_id()}" - } - } - } - - if !MediaManager::is_webrtc_supported() { - div { - class: "warning-message", - "⚠️ WebRTC wird von diesem Browser nicht unterstützt" + connected, + audio_enabled, + local_peer_id, + remote_peer_id, + media_manager } } } diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..4187484 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,3 @@ +mod signaling_message; + +pub use signaling_message::SignalingMessage; diff --git a/src/models/signaling_message.rs b/src/models/signaling_message.rs new file mode 100644 index 0000000..24c7a6b --- /dev/null +++ b/src/models/signaling_message.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SignalingMessage { + pub from: String, + pub to: String, + pub msg_type: String, + pub data: String, +} diff --git a/src/utils/media_manager.rs b/src/utils/media_manager.rs index 5e16fe9..f680263 100644 --- a/src/utils/media_manager.rs +++ b/src/utils/media_manager.rs @@ -1,6 +1,10 @@ use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; -use web_sys::{MediaDevices, MediaStream, MediaStreamConstraints, Navigator, Window}; +use web_sys::{ + MediaStream, MediaStreamConstraints, Navigator, Window, + RtcIceCandidate, RtcPeerConnection, RtcConfiguration, RtcIceServer +}; // Enum für verschiedene Media-Zustände #[derive(Debug, Clone, PartialEq)] @@ -24,6 +28,22 @@ impl MediaManager { state: MediaState::Uninitialized, } } + + pub fn create_peer_connection() -> Result { + // STUN-Server configuration + let ice_server = RtcIceServer::new(); + let urls = js_sys::Array::new(); + urls.push(&JsValue::from_str("stun:stun.l.google.com:19302")); + ice_server.set_urls(&urls.into()); + + let config = RtcConfiguration::new(); + let ice_servers = js_sys::Array::new(); + ice_servers.push(&ice_server.into()); + config.set_ice_servers(&ice_servers.into()); + + RtcPeerConnection::new_with_configuration(&config) + .map_err(|e| format!("Failed to create peer connection: {:?}", e)) + } // Checks if WebRTC is supported pub fn is_webrtc_supported() -> bool { @@ -51,9 +71,9 @@ impl MediaManager { let media_devices = navigator.media_devices().map_err(|_| "MediaDevices API nicht verfügbar")?; // Define media constraints: only audio, no video - let mut constraints = MediaStreamConstraints::new(); - constraints.audio(&JsValue::from(true)); - constraints.video(&JsValue::from(false)); + let constraints = MediaStreamConstraints::new(); + constraints.set_audio(&JsValue::from(true)); + constraints.set_video(&JsValue::from(false)); // Request access to the microphone let promise = media_devices