Project structure refactoring. Added connection to signaling server.
This commit is contained in:
parent
41affe1aaa
commit
dc71f02fc7
12
Cargo.toml
12
Cargo.toml
@ -18,6 +18,7 @@ js-sys = "0.3.61"
|
|||||||
# web-sys with features for media devices
|
# web-sys with features for media devices
|
||||||
web-sys = { version = "0.3.77", features = [
|
web-sys = { version = "0.3.77", features = [
|
||||||
"BinaryType",
|
"BinaryType",
|
||||||
|
"ErrorEvent",
|
||||||
"Navigator",
|
"Navigator",
|
||||||
"MediaDevices",
|
"MediaDevices",
|
||||||
"MediaStream",
|
"MediaStream",
|
||||||
@ -25,7 +26,16 @@ web-sys = { version = "0.3.77", features = [
|
|||||||
"MediaStreamTrack",
|
"MediaStreamTrack",
|
||||||
"MediaTrackSettings",
|
"MediaTrackSettings",
|
||||||
"MediaTrackConstraints",
|
"MediaTrackConstraints",
|
||||||
"AudioContext"
|
"AudioContext",
|
||||||
|
"RtcPeerConnection",
|
||||||
|
"RtcConfiguration",
|
||||||
|
"RtcIceServer",
|
||||||
|
"RtcIceCandidate",
|
||||||
|
"RtcIceCandidateInit",
|
||||||
|
"RtcSessionDescription",
|
||||||
|
"RtcSessionDescriptionInit",
|
||||||
|
"RtcOfferOptions",
|
||||||
|
"RtcAnswerOptions"
|
||||||
]}
|
]}
|
||||||
|
|
||||||
# Logging and Tracing
|
# Logging and Tracing
|
||||||
|
|||||||
141
src/components/call_controls.rs
Normal file
141
src/components/call_controls.rs
Normal file
@ -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<bool>,
|
||||||
|
audio_enabled: Signal<bool>,
|
||||||
|
media_manager: Signal<MediaManager>,
|
||||||
|
web_socket: Signal<Option<BrowserWebSocket>>,
|
||||||
|
local_peer_id: Signal<String>,
|
||||||
|
remote_peer_id: Signal<String>,
|
||||||
|
) -> 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/components/connection_panel.rs
Normal file
76
src/components/connection_panel.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use web_sys::WebSocket as BrowserWebSocket;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ConnectionPanel(
|
||||||
|
connected: Signal<bool>,
|
||||||
|
local_peer_id: Signal<String>,
|
||||||
|
remote_peer_id: Signal<String>,
|
||||||
|
web_socket: Signal<Option<BrowserWebSocket>>,
|
||||||
|
) -> 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..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/components/mod.rs
Normal file
7
src/components/mod.rs
Normal file
@ -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;
|
||||||
131
src/components/status_display.rs
Normal file
131
src/components/status_display.rs
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use crate::utils::{MediaManager, MediaState};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn StatusDisplay(
|
||||||
|
connected: Signal<bool>,
|
||||||
|
audio_enabled: Signal<bool>,
|
||||||
|
local_peer_id: Signal<String>,
|
||||||
|
remote_peer_id: Signal<String>,
|
||||||
|
media_manager: Signal<MediaManager>,
|
||||||
|
) -> 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
457
src/main.rs
457
src/main.rs
@ -1,19 +1,25 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
mod components;
|
||||||
|
mod models;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use dioxus::{html::{g::media, h3}, prelude::*};
|
use dioxus::prelude::*;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen::prelude::Closure;
|
use wasm_bindgen::prelude::{Closure, JsValue};
|
||||||
use web_sys::{BinaryType, MessageEvent, WebSocket as BrowserWebSocket};
|
use web_sys::{BinaryType, MessageEvent, WebSocket as BrowserWebSocket};
|
||||||
|
|
||||||
|
use components::{ConnectionPanel, CallControls, StatusDisplay};
|
||||||
use utils::{MediaManager, MediaState};
|
use utils::{MediaManager, MediaState};
|
||||||
|
use models::SignalingMessage;
|
||||||
|
|
||||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||||
const MAIN_CSS: Asset = asset!("/assets/main.css");
|
const MAIN_CSS: Asset = asset!("/assets/main.css");
|
||||||
const HEADER_SVG: Asset = asset!("/assets/header.svg");
|
|
||||||
|
|
||||||
fn main() {
|
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);
|
dioxus::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,368 +34,151 @@ fn App() -> Element {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Content() ->Element {
|
pub fn Content() ->Element {
|
||||||
// State for connection status and audio
|
// Initialize signals
|
||||||
let mut connected = use_signal(|| false);
|
let connected = use_signal(|| false);
|
||||||
let mut audio_enabled = use_signal(|| true);
|
let audio_enabled = use_signal(|| true);
|
||||||
|
let local_peer_id = use_signal(|| generate_peer_id());
|
||||||
// State for Peer IDs
|
let remote_peer_id = use_signal(|| String::new());
|
||||||
let mut local_peer_id = use_signal(|| generate_peer_id());
|
let media_manager = use_signal(|| MediaManager::new());
|
||||||
let mut remote_peer_id = use_signal(|| String::new());
|
let web_socket= use_signal(|| None::<BrowserWebSocket>);
|
||||||
|
|
||||||
let mut media_manager = use_signal(|| MediaManager::new());
|
|
||||||
|
|
||||||
// On mount: Request microphone access if not already granted
|
// On mount: Request microphone access if not already granted
|
||||||
use_effect(move || {
|
use_effect(move || {
|
||||||
to_owned![media_manager];
|
to_owned![media_manager];
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
match media_manager.write().request_microphone_access().await {
|
if let Ok(mut manager) = media_manager.try_write() {
|
||||||
|
match manager.request_microphone_access().await {
|
||||||
Ok(_) => log::info!("Microphone access granted"),
|
Ok(_) => log::info!("Microphone access granted"),
|
||||||
Err(e) => log::error!("Failed to request microphone access: {}", e)
|
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<dyn FnMut(web_sys::ErrorEvent)>);
|
||||||
|
|
||||||
|
// 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<dyn FnMut(web_sys::CloseEvent)>)
|
||||||
|
};
|
||||||
|
|
||||||
|
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::<SignalingMessage>(&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<dyn FnMut(MessageEvent)>)
|
||||||
|
};
|
||||||
|
|
||||||
|
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<dyn FnMut(web_sys::Event)>)
|
||||||
|
};
|
||||||
|
|
||||||
|
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<dyn FnMut(web_sys::CloseEvent)>)
|
||||||
|
};
|
||||||
|
|
||||||
|
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! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: "app-container",
|
class: "app-container",
|
||||||
|
|
||||||
// Header
|
|
||||||
header {
|
header {
|
||||||
h1 { "Voice Chat MVP" }
|
h1 { "Voice Chat MVP" }
|
||||||
p { "WebRTC-basierter Sprachchat mit Ende-zu-Ende-Verschlüsselung" }
|
p { "WebRTC-basierter Sprachchat mit Ende-zu-Ende-Verschlüsselung" }
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
class: "main-content",
|
class: "main-content",
|
||||||
|
|
||||||
// Connection Panel
|
// Connection Panel
|
||||||
ConnectionPanel {
|
ConnectionPanel {
|
||||||
connected: connected.clone(),
|
connected,
|
||||||
local_peer_id: local_peer_id.clone(),
|
local_peer_id,
|
||||||
remote_peer_id: remote_peer_id.clone()
|
remote_peer_id,
|
||||||
|
web_socket
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call Controls
|
// Call Controls
|
||||||
CallControls {
|
CallControls {
|
||||||
connected: connected.clone(),
|
connected,
|
||||||
audio_enabled: audio_enabled.clone(),
|
audio_enabled,
|
||||||
media_manager: media_manager.clone(),
|
media_manager,
|
||||||
on_start_call: move |_| {
|
web_socket,
|
||||||
log::info!("Anruf wird gestartet mit Remote Peer: {}", remote_peer_id());
|
local_peer_id,
|
||||||
},
|
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" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status Display
|
// Status Display
|
||||||
StatusDisplay {
|
StatusDisplay {
|
||||||
connected: connected.clone(),
|
connected,
|
||||||
audio_enabled: audio_enabled.clone(),
|
audio_enabled,
|
||||||
local_peer_id: local_peer_id.clone(),
|
local_peer_id,
|
||||||
remote_peer_id: remote_peer_id.clone(),
|
remote_peer_id,
|
||||||
media_manager: media_manager.clone()
|
media_manager
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Komponente für Verbindungseinstellungen
|
|
||||||
#[component]
|
|
||||||
fn ConnectionPanel(
|
|
||||||
connected: Signal<bool>,
|
|
||||||
local_peer_id: Signal<String>,
|
|
||||||
remote_peer_id: Signal<String>
|
|
||||||
) -> Element {
|
|
||||||
// Websocket signal
|
|
||||||
let ws = use_signal(|| None::<BrowserWebSocket>);
|
|
||||||
|
|
||||||
// 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<dyn FnMut(MessageEvent)>);
|
|
||||||
|
|
||||||
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<bool>,
|
|
||||||
audio_enabled: Signal<bool>,
|
|
||||||
mut media_manager: Signal<MediaManager>,
|
|
||||||
on_start_call: EventHandler<MouseEvent>,
|
|
||||||
on_end_call: EventHandler<MouseEvent>,
|
|
||||||
on_toggle_audio: EventHandler<MouseEvent>
|
|
||||||
) -> 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<bool>,
|
|
||||||
audio_enabled: Signal<bool>,
|
|
||||||
local_peer_id: Signal<String>,
|
|
||||||
remote_peer_id: Signal<String>,
|
|
||||||
media_manager: Signal<MediaManager>
|
|
||||||
) -> 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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/models/mod.rs
Normal file
3
src/models/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod signaling_message;
|
||||||
|
|
||||||
|
pub use signaling_message::SignalingMessage;
|
||||||
9
src/models/signaling_message.rs
Normal file
9
src/models/signaling_message.rs
Normal file
@ -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,
|
||||||
|
}
|
||||||
@ -1,6 +1,10 @@
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen_futures::JsFuture;
|
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
|
// Enum für verschiedene Media-Zustände
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@ -25,6 +29,22 @@ impl MediaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_peer_connection() -> Result<RtcPeerConnection, String> {
|
||||||
|
// 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
|
// Checks if WebRTC is supported
|
||||||
pub fn is_webrtc_supported() -> bool {
|
pub fn is_webrtc_supported() -> bool {
|
||||||
let window: Window = match web_sys::window() {
|
let window: Window = match web_sys::window() {
|
||||||
@ -51,9 +71,9 @@ impl MediaManager {
|
|||||||
let media_devices = navigator.media_devices().map_err(|_| "MediaDevices API nicht verfügbar")?;
|
let media_devices = navigator.media_devices().map_err(|_| "MediaDevices API nicht verfügbar")?;
|
||||||
|
|
||||||
// Define media constraints: only audio, no video
|
// Define media constraints: only audio, no video
|
||||||
let mut constraints = MediaStreamConstraints::new();
|
let constraints = MediaStreamConstraints::new();
|
||||||
constraints.audio(&JsValue::from(true));
|
constraints.set_audio(&JsValue::from(true));
|
||||||
constraints.video(&JsValue::from(false));
|
constraints.set_video(&JsValue::from(false));
|
||||||
|
|
||||||
// Request access to the microphone
|
// Request access to the microphone
|
||||||
let promise = media_devices
|
let promise = media_devices
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user