feat: centralize signaling service and update ui
This commit is contained in:
parent
328d5d1003
commit
a75b995cb0
@ -20,6 +20,7 @@ web-sys = { version = "0.3.77", features = [
|
|||||||
"BinaryType",
|
"BinaryType",
|
||||||
"ErrorEvent",
|
"ErrorEvent",
|
||||||
"Navigator",
|
"Navigator",
|
||||||
|
"Clipboard",
|
||||||
"MediaDevices",
|
"MediaDevices",
|
||||||
"MediaStream",
|
"MediaStream",
|
||||||
"MediaStreamConstraints",
|
"MediaStreamConstraints",
|
||||||
|
|||||||
6
assets/appsettings.json
Normal file
6
assets/appsettings.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"stun_server": "stun:stun.l.google.com:19302",
|
||||||
|
"signaling_url": "ws://localhost:1900/ws"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,196 +1,76 @@
|
|||||||
use crate::models::SignalingMessage;
|
use crate::services::signaling::use_signaling;
|
||||||
use crate::utils::MediaManager;
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use wasm_bindgen::prelude::Closure;
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
use wasm_bindgen::JsValue;
|
|
||||||
use web_sys::{MediaStream, RtcPeerConnection, WebSocket as BrowserWebSocket};
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CallControls(
|
pub fn CallControls() -> Element {
|
||||||
peer_id: Signal<String>,
|
let service = use_signaling();
|
||||||
remote_id: Signal<String>,
|
let state = service.state.clone();
|
||||||
connected: Signal<bool>,
|
let actions = service.actions.clone();
|
||||||
websocket: Signal<Option<BrowserWebSocket>>,
|
|
||||||
peer_connection: Signal<Option<RtcPeerConnection>>, // **INITIATOR CONNECTION**
|
let peer_id_value = state.peer_id.read().clone();
|
||||||
local_media: Signal<Option<MediaStream>>,
|
let remote_id_value = state.remote_id.read().clone();
|
||||||
) -> Element {
|
let remote_id_for_label = remote_id_value.clone();
|
||||||
let mic_granted = use_signal(|| false);
|
let connected = *state.connected.read();
|
||||||
|
let mic_ready = *state.mic_granted.read();
|
||||||
|
let in_call = *state.in_call.read();
|
||||||
|
let has_target = !remote_id_value.is_empty();
|
||||||
|
|
||||||
|
let request_microphone = actions.request_microphone.clone();
|
||||||
|
let start_call = actions.start_call.clone();
|
||||||
|
let leave_call = actions.leave_call.clone();
|
||||||
|
|
||||||
let mut audio_muted = use_signal(|| false);
|
let mut audio_muted = use_signal(|| false);
|
||||||
let mut in_call = use_signal(|| false);
|
let mut mute_signal = audio_muted.clone();
|
||||||
|
let mut reset_signal = audio_muted.clone();
|
||||||
|
let muted = *audio_muted.read();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "call-controls",
|
div { class: "call-controls",
|
||||||
div { class: "call-controls__left",
|
div { class: "call-controls__left",
|
||||||
span { class: "self-pill", "Your ID: {peer_id.read()}" }
|
span { class: "self-pill", "Your ID: {peer_id_value}" }
|
||||||
if !remote_id.read().is_empty() {
|
if !remote_id_for_label.is_empty() {
|
||||||
span { class: "self-pill self-pill--target", "Target: {remote_id.read()}" }
|
span { class: "self-pill self-pill--target", "Target: {remote_id_for_label}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div { class: "call-controls__center",
|
div { class: "call-controls__center",
|
||||||
button {
|
button {
|
||||||
class: if *mic_granted.read() { "ctrl-btn ctrl-btn--secondary" } else { "ctrl-btn ctrl-btn--primary" },
|
class: if mic_ready { "ctrl-btn ctrl-btn--secondary" } else { "ctrl-btn ctrl-btn--primary" },
|
||||||
disabled: *mic_granted.read(),
|
disabled: mic_ready,
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
log::info!("Requesting microphone permission");
|
log::info!("Requesting microphone permission");
|
||||||
let mut mm_state = mic_granted.clone();
|
request_microphone();
|
||||||
let pc_signal = peer_connection.clone();
|
|
||||||
let mut local_media_signal = local_media.clone();
|
|
||||||
spawn(async move {
|
|
||||||
let mut manager = crate::utils::MediaManager::new();
|
|
||||||
match manager.request_microphone_access().await {
|
|
||||||
Ok(stream) => {
|
|
||||||
log::info!("Microphone granted");
|
|
||||||
mm_state.set(true);
|
|
||||||
local_media_signal.set(Some(stream.clone()));
|
|
||||||
if let Some(pc) = pc_signal.read().as_ref() {
|
|
||||||
if let Err(e) = crate::utils::MediaManager::add_stream_to_pc(pc, &stream) {
|
|
||||||
log::warn!("Failed to attach local tracks: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Microphone request failed: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
if *mic_granted.read() { "Mic ready" } else { "Enable mic" }
|
if mic_ready { "Mic ready" } else { "Enable mic" }
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: "ctrl-btn ctrl-btn--primary",
|
class: "ctrl-btn ctrl-btn--primary",
|
||||||
disabled: !*mic_granted.read() || !*connected.read() || remote_id.read().is_empty(),
|
disabled: !mic_ready || !connected || !has_target || in_call,
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
|
if in_call {
|
||||||
|
return;
|
||||||
|
}
|
||||||
log::info!("Launching WebRTC call as initiator");
|
log::info!("Launching WebRTC call as initiator");
|
||||||
|
audio_muted.set(false);
|
||||||
let mut pc_signal = peer_connection.clone();
|
start_call();
|
||||||
let ws_signal = websocket.clone();
|
|
||||||
let from_id = peer_id.read().clone();
|
|
||||||
let to_id = remote_id.read().clone();
|
|
||||||
let mut in_call_flag = in_call.clone();
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
let pc = if pc_signal.read().is_none() {
|
|
||||||
match MediaManager::create_peer_connection() {
|
|
||||||
Ok(new_pc) => {
|
|
||||||
let ws_clone = ws_signal.clone();
|
|
||||||
let to_clone = to_id.clone();
|
|
||||||
let from_clone = from_id.clone();
|
|
||||||
let on_ice = Closure::wrap(Box::new(move |ev: JsValue| {
|
|
||||||
if let Ok(candidate_val) = js_sys::Reflect::get(&ev, &JsValue::from_str("candidate")) {
|
|
||||||
if candidate_val.is_null() || candidate_val.is_undefined() { return; }
|
|
||||||
if let Some(ws) = ws_clone.read().as_ref() {
|
|
||||||
if let Ok(json_js) = js_sys::JSON::stringify(&candidate_val) {
|
|
||||||
if let Some(json) = json_js.as_string() {
|
|
||||||
let msg = SignalingMessage {
|
|
||||||
from: from_clone.clone(),
|
|
||||||
to: to_clone.clone(),
|
|
||||||
msg_type: "candidate".to_string(),
|
|
||||||
data: json,
|
|
||||||
};
|
|
||||||
if let Ok(text) = serde_json::to_string(&msg) { let _ = ws.send_with_str(&text); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as Box<dyn FnMut(JsValue)>);
|
|
||||||
new_pc.set_onicecandidate(Some(on_ice.as_ref().unchecked_ref()));
|
|
||||||
on_ice.forget();
|
|
||||||
|
|
||||||
let on_track = Closure::wrap(Box::new(move |ev: JsValue| {
|
|
||||||
if let Ok(streams_val) = js_sys::Reflect::get(&ev, &JsValue::from_str("streams")) {
|
|
||||||
if streams_val.is_undefined() || streams_val.is_null() { return; }
|
|
||||||
let streams_array = js_sys::Array::from(&streams_val);
|
|
||||||
let first = streams_array.get(0);
|
|
||||||
if let Ok(stream) = first.clone().dyn_into::<web_sys::MediaStream>() {
|
|
||||||
if let Some(window) = web_sys::window() {
|
|
||||||
if let Some(document) = window.document() {
|
|
||||||
if let Ok(audio_el) = document.create_element("audio") {
|
|
||||||
if let Ok(audio) = audio_el.dyn_into::<web_sys::HtmlAudioElement>() {
|
|
||||||
audio.set_autoplay(true);
|
|
||||||
audio.set_src_object(Some(&stream));
|
|
||||||
if let Some(body) = document.body() {
|
|
||||||
let _ = body.append_child(&audio).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as Box<dyn FnMut(JsValue)>);
|
|
||||||
new_pc.set_ontrack(Some(on_track.as_ref().unchecked_ref()));
|
|
||||||
on_track.forget();
|
|
||||||
|
|
||||||
pc_signal.set(Some(new_pc.clone()));
|
|
||||||
log::info!("Initiator PeerConnection ready");
|
|
||||||
new_pc
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to create initiator peer connection: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pc_signal.read().as_ref().unwrap().clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(local) = local_media.read().as_ref() {
|
|
||||||
if let Err(e) = crate::utils::MediaManager::add_stream_to_pc(&pc, local) {
|
|
||||||
log::warn!("Failed to attach local tracks before offer: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match MediaManager::create_offer(&pc).await {
|
|
||||||
Ok(offer_sdp) => {
|
|
||||||
if let Some(socket) = ws_signal.read().as_ref() {
|
|
||||||
let msg = SignalingMessage {
|
|
||||||
from: from_id.clone(),
|
|
||||||
to: to_id.clone(),
|
|
||||||
msg_type: "offer".to_string(),
|
|
||||||
data: offer_sdp,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(json) = serde_json::to_string(&msg) {
|
|
||||||
let _ = socket.send_with_str(&json);
|
|
||||||
log::info!("Offer dispatched to {}", to_id);
|
|
||||||
in_call_flag.set(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => log::error!("Offer creation failed: {}", e),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
"Start call"
|
if in_call { "In call" } else { "Start call" }
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: if *audio_muted.read() { "ctrl-btn ctrl-btn--muted" } else { "ctrl-btn" },
|
class: if muted { "ctrl-btn ctrl-btn--muted" } else { "ctrl-btn" },
|
||||||
disabled: !*in_call.read(),
|
disabled: !in_call,
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let current_muted = *audio_muted.read();
|
let current = *mute_signal.read();
|
||||||
audio_muted.set(!current_muted);
|
mute_signal.set(!current);
|
||||||
log::info!("Audio {}", if !current_muted { "muted" } else { "unmuted" });
|
log::info!("Audio {}", if current { "unmuted" } else { "muted" });
|
||||||
},
|
},
|
||||||
if *audio_muted.read() { "Unmute" } else { "Mute" }
|
if muted { "Unmute" } else { "Mute" }
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: "ctrl-btn ctrl-btn--danger",
|
class: "ctrl-btn ctrl-btn--danger",
|
||||||
disabled: !*in_call.read(),
|
disabled: !in_call,
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
in_call.set(false);
|
reset_signal.set(false);
|
||||||
audio_muted.set(false);
|
leave_call();
|
||||||
|
|
||||||
let has_peer_connection = peer_connection.read().is_some();
|
|
||||||
|
|
||||||
if has_peer_connection {
|
|
||||||
if let Some(pc) = peer_connection.read().as_ref() {
|
|
||||||
pc.close();
|
|
||||||
log::info!("Initiator PeerConnection closed");
|
|
||||||
}
|
|
||||||
peer_connection.set(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Call ended");
|
log::info!("Call ended");
|
||||||
},
|
},
|
||||||
"Leave"
|
"Leave"
|
||||||
@ -198,7 +78,13 @@ pub fn CallControls(
|
|||||||
}
|
}
|
||||||
div { class: "call-controls__right",
|
div { class: "call-controls__right",
|
||||||
span { class: "connection-hint",
|
span { class: "connection-hint",
|
||||||
if *connected.read() { "Connected to signaling" } else { "Waiting for signaling" }
|
if !connected {
|
||||||
|
"Waiting for signaling"
|
||||||
|
} else if in_call {
|
||||||
|
"In active call"
|
||||||
|
} else {
|
||||||
|
"Connected to signaling"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,394 +1,69 @@
|
|||||||
use crate::config::Config;
|
use crate::services::signaling::use_signaling;
|
||||||
use crate::models::SignalingMessage;
|
|
||||||
use crate::utils::MediaManager;
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use futures::StreamExt;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use wasm_bindgen::prelude::Closure;
|
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||||
use web_sys::MediaStream;
|
|
||||||
use web_sys::{BinaryType, MessageEvent, RtcPeerConnection, WebSocket as BrowserWebSocket};
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ConnectionPanel(
|
pub fn ConnectionPanel() -> Element {
|
||||||
mut peer_id: Signal<String>,
|
let service = use_signaling();
|
||||||
mut remote_id: Signal<String>,
|
let state = service.state.clone();
|
||||||
mut connected: Signal<bool>,
|
let actions = service.actions.clone();
|
||||||
mut websocket: Signal<Option<BrowserWebSocket>>,
|
|
||||||
peer_connection: Signal<Option<RtcPeerConnection>>, // **RESPONDER CONNECTION**
|
|
||||||
initiator_connection: Signal<Option<RtcPeerConnection>>, // Initiator PC (wird für eingehende Answers verwendet)
|
|
||||||
local_media: Signal<Option<MediaStream>>,
|
|
||||||
) -> Element {
|
|
||||||
let ws_status = use_signal(|| "Nicht verbunden".to_string());
|
|
||||||
// Buffer for an incoming Answer SDP if the initiator PC isn't ready yet
|
|
||||||
let pending_answer = use_signal(|| None::<String>);
|
|
||||||
let cfg_signal: Signal<Config> = use_context();
|
|
||||||
|
|
||||||
// **COROUTINE** für Offer-Handling (Responder empfängt Offers)
|
let connected = *state.connected.read();
|
||||||
let offer_handler = use_coroutine(move |mut rx| async move {
|
let ws_status = state.ws_status.read().clone();
|
||||||
while let Some(msg) = rx.next().await {
|
let peer_id_value = state.peer_id.read().clone();
|
||||||
let SignalingMessage {
|
let peer_id_for_copy = peer_id_value.clone();
|
||||||
from,
|
let remote_id_value = state.remote_id.read().clone();
|
||||||
to,
|
let mic_ready = *state.mic_granted.read();
|
||||||
msg_type,
|
let in_call = *state.in_call.read();
|
||||||
data,
|
|
||||||
} = msg;
|
|
||||||
|
|
||||||
// **KORREKT:** In der Coroutine-Loop
|
let reconnect = actions.connect.clone();
|
||||||
if msg_type == "offer" {
|
let set_remote_id_action = actions.set_remote_id.clone();
|
||||||
log::info!("📞 WebRTC-Offer von {} als Responder verarbeiten", from);
|
|
||||||
|
|
||||||
// **WICHTIG:** Clone für später aufbewahren
|
|
||||||
let from_clone = from.clone();
|
|
||||||
|
|
||||||
// **RESPONDER:** PeerConnection erstellen
|
|
||||||
let pc = if peer_connection.read().is_none() {
|
|
||||||
match MediaManager::create_peer_connection() {
|
|
||||||
Ok(new_pc) => {
|
|
||||||
// Attach onicecandidate handler to send candidates via websocket
|
|
||||||
let ws_clone = websocket.clone();
|
|
||||||
let from_for_ice = from_clone.clone();
|
|
||||||
let on_ice = Closure::wrap(Box::new(move |ev: JsValue| {
|
|
||||||
// ev.candidate may be null/undefined or an object
|
|
||||||
if let Ok(candidate_val) =
|
|
||||||
js_sys::Reflect::get(&ev, &JsValue::from_str("candidate"))
|
|
||||||
{
|
|
||||||
if candidate_val.is_null() || candidate_val.is_undefined() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some(ws) = ws_clone.read().as_ref() {
|
|
||||||
// Try to stringify the candidate object directly
|
|
||||||
if let Ok(json_js) = js_sys::JSON::stringify(&candidate_val)
|
|
||||||
{
|
|
||||||
if let Some(json) = json_js.as_string() {
|
|
||||||
let msg = crate::models::SignalingMessage {
|
|
||||||
from: peer_id.read().clone(),
|
|
||||||
to: from_for_ice.clone(),
|
|
||||||
msg_type: "candidate".to_string(),
|
|
||||||
data: json,
|
|
||||||
};
|
|
||||||
if let Ok(text) = serde_json::to_string(&msg) {
|
|
||||||
let _ = ws.send_with_str(&text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
as Box<dyn FnMut(JsValue)>);
|
|
||||||
new_pc.set_onicecandidate(Some(on_ice.as_ref().unchecked_ref()));
|
|
||||||
on_ice.forget();
|
|
||||||
|
|
||||||
// ontrack -> play remote audio
|
|
||||||
let on_track = Closure::wrap(Box::new(move |ev: JsValue| {
|
|
||||||
// ev.streams is an array of MediaStream
|
|
||||||
if let Ok(streams_val) =
|
|
||||||
js_sys::Reflect::get(&ev, &JsValue::from_str("streams"))
|
|
||||||
{
|
|
||||||
if streams_val.is_undefined() || streams_val.is_null() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let streams_array = js_sys::Array::from(&streams_val);
|
|
||||||
let first = streams_array.get(0);
|
|
||||||
let stream_js = first.clone();
|
|
||||||
if let Ok(stream) = stream_js.dyn_into::<web_sys::MediaStream>()
|
|
||||||
{
|
|
||||||
if let Some(window) = web_sys::window() {
|
|
||||||
if let Some(document) = window.document() {
|
|
||||||
if let Ok(audio_el) =
|
|
||||||
document.create_element("audio")
|
|
||||||
{
|
|
||||||
if let Ok(audio) = audio_el
|
|
||||||
.dyn_into::<web_sys::HtmlAudioElement>(
|
|
||||||
) {
|
|
||||||
audio.set_autoplay(true);
|
|
||||||
audio.set_src_object(Some(&stream));
|
|
||||||
if let Some(body) = document.body() {
|
|
||||||
let _ = body.append_child(&audio).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
as Box<dyn FnMut(JsValue)>);
|
|
||||||
new_pc.set_ontrack(Some(on_track.as_ref().unchecked_ref()));
|
|
||||||
on_track.forget();
|
|
||||||
|
|
||||||
peer_connection.set(Some(new_pc.clone()));
|
|
||||||
let from_for_log = from_clone.clone();
|
|
||||||
log::info!("✅ Responder PeerConnection für {} erstellt", from_for_log);
|
|
||||||
new_pc
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("❌ Responder PeerConnection-Fehler: {}", e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
peer_connection.read().as_ref().unwrap().clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Offer verarbeiten und Answer erstellen
|
|
||||||
match MediaManager::handle_offer(&pc, &data).await {
|
|
||||||
Ok(answer_sdp) => {
|
|
||||||
log::info!("✅ Responder Answer erstellt, sende zurück...");
|
|
||||||
|
|
||||||
if let Some(socket) = websocket.read().as_ref() {
|
|
||||||
let answer_msg = SignalingMessage {
|
|
||||||
from: to, // ← to wird moved
|
|
||||||
to: from, // ← from wird hier moved (Original)
|
|
||||||
msg_type: "answer".to_string(),
|
|
||||||
data: answer_sdp,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(json) = serde_json::to_string(&answer_msg) {
|
|
||||||
let _ = socket.send_with_str(&json);
|
|
||||||
log::info!("📤 Responder Answer gesendet an {}", from_clone);
|
|
||||||
// ✅ Clone verwenden
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => log::error!("❌ Responder Answer-Fehler: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Peer-ID generieren
|
|
||||||
use_effect(move || {
|
|
||||||
use js_sys::{Date, Math};
|
|
||||||
let timestamp = Date::now() as u64;
|
|
||||||
let random = (Math::random() * 900.0 + 100.0) as u32;
|
|
||||||
let id = format!("peer-{}-{}", timestamp, random);
|
|
||||||
peer_id.set(id.clone());
|
|
||||||
log::info!("🆔 Peer-ID generiert: {}", id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Einfacher Status-String für das Mikrofon
|
|
||||||
let mic_status = if local_media.read().is_some() {
|
|
||||||
"Granted"
|
|
||||||
} else {
|
|
||||||
"Not granted"
|
|
||||||
};
|
|
||||||
|
|
||||||
// WebSocket verbinden
|
|
||||||
let connect_logic: Rc<dyn Fn()> = {
|
|
||||||
let ws_status_signal = ws_status.clone();
|
|
||||||
let connected_signal = connected.clone();
|
|
||||||
let websocket_signal = websocket.clone();
|
|
||||||
let offer_handler = offer_handler.clone();
|
|
||||||
let pending_answer_handle = pending_answer.clone();
|
|
||||||
let cfg_signal_handle = cfg_signal.clone();
|
|
||||||
let initiator_connection_signal = initiator_connection.clone();
|
|
||||||
let peer_connection_signal = peer_connection.clone();
|
|
||||||
|
|
||||||
Rc::new(move || {
|
|
||||||
let mut ws_status = ws_status_signal.clone();
|
|
||||||
let connected = connected_signal.clone();
|
|
||||||
let mut websocket = websocket_signal.clone();
|
|
||||||
let cfg_signal = cfg_signal_handle.clone();
|
|
||||||
let initiator_connection = initiator_connection_signal.clone();
|
|
||||||
let peer_connection = peer_connection_signal.clone();
|
|
||||||
let pending_answer = pending_answer_handle.clone();
|
|
||||||
|
|
||||||
if *connected.read() || websocket.read().is_some() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ws_status.set("Verbinde...".to_string());
|
|
||||||
|
|
||||||
let endpoint = cfg_signal.read().server.signaling_url.trim().to_string();
|
|
||||||
let target = if endpoint.is_empty() {
|
|
||||||
crate::constants::DEFAULT_SIGNALING_URL.to_string()
|
|
||||||
} else {
|
|
||||||
endpoint
|
|
||||||
};
|
|
||||||
|
|
||||||
log::info!("🔌 Verbinde WebSocket zu {}", target);
|
|
||||||
|
|
||||||
match BrowserWebSocket::new(&target) {
|
|
||||||
Ok(socket) => {
|
|
||||||
socket.set_binary_type(BinaryType::Arraybuffer);
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
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()));
|
|
||||||
|
|
||||||
onopen.forget();
|
|
||||||
onclose.forget();
|
|
||||||
onmessage.forget();
|
|
||||||
|
|
||||||
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.
|
|
||||||
{
|
|
||||||
let mut pending_sig = pending_answer.clone();
|
|
||||||
let init_conn = initiator_connection.clone();
|
|
||||||
use_effect(move || {
|
|
||||||
// Clone out the buffered answer quickly to avoid holding the read-borrow
|
|
||||||
let maybe = pending_sig.read().clone();
|
|
||||||
if let Some(answer_sdp) = maybe {
|
|
||||||
if let Some(pc) = init_conn.read().as_ref() {
|
|
||||||
let pc_clone = pc.clone();
|
|
||||||
let answer_clone = answer_sdp.clone();
|
|
||||||
spawn_local(async move {
|
|
||||||
match crate::utils::MediaManager::handle_answer(&pc_clone, &answer_clone)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => log::info!(
|
|
||||||
"✅ Gepufferte Answer erfolgreich gesetzt auf Initiator-PC"
|
|
||||||
),
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("❌ Fehler beim Setzen der gepufferten Answer: {}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Clear buffer
|
|
||||||
pending_sig.set(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "connection-panel",
|
div { class: "connection-panel",
|
||||||
section { class: "connection-card",
|
section { class: "connection-card",
|
||||||
header { class: "connection-card__header",
|
header { class: "connection-card__header",
|
||||||
h3 { "Connection" }
|
h3 { "Connection" }
|
||||||
span { class: if *connected.read() { "pill pill--success" } else { "pill pill--danger" }, "{ws_status.read()}" }
|
span {
|
||||||
|
class: if connected { "pill pill--success" } else { "pill pill--danger" },
|
||||||
|
"{ws_status}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ul { class: "connection-status-list",
|
ul { class: "connection-status-list",
|
||||||
li {
|
li {
|
||||||
span { class: "label", "WebSocket" }
|
span { class: "label", "WebSocket" }
|
||||||
span {
|
span {
|
||||||
class: if *connected.read() { "value value--success" } else { "value value--danger" },
|
class: if connected { "value value--success" } else { "value value--danger" },
|
||||||
if *connected.read() { "Connected" } else { "Disconnected" }
|
if connected { "Connected" } else { "Disconnected" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
span { class: "label", "Microphone" }
|
span { class: "label", "Microphone" }
|
||||||
span {
|
span {
|
||||||
class: if local_media.read().is_some() { "value value--success" } else { "value value--danger" },
|
class: if mic_ready { "value value--success" } else { "value value--danger" },
|
||||||
"{mic_status}"
|
if mic_ready { "Ready" } else { "Not requested" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
li {
|
||||||
|
span { class: "label", "Call" }
|
||||||
|
span {
|
||||||
|
class: if in_call { "value value--success" } else { "value" },
|
||||||
|
if in_call { "In call" } else { "Idle" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { class: "connection-card__actions",
|
||||||
|
button {
|
||||||
|
class: "ctrl-btn ctrl-btn--secondary",
|
||||||
|
disabled: connected,
|
||||||
|
onclick: move |_| {
|
||||||
|
log::info!("Manual reconnect requested");
|
||||||
|
(reconnect)();
|
||||||
|
},
|
||||||
|
"Reconnect"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,13 +77,39 @@ pub fn ConnectionPanel(
|
|||||||
input {
|
input {
|
||||||
class: "input input--readonly",
|
class: "input input--readonly",
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
value: "{peer_id.read()}",
|
value: "{peer_id_value}",
|
||||||
readonly: true
|
readonly: true
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: "icon-btn",
|
class: "icon-btn",
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
log::info!("📋 Peer-ID kopiert: {}", peer_id.read());
|
let copy_target = peer_id_for_copy.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
let navigator = window.navigator();
|
||||||
|
match js_sys::Reflect::get(&navigator, &JsValue::from_str("clipboard")) {
|
||||||
|
Ok(handle) if !handle.is_undefined() && !handle.is_null() => {
|
||||||
|
match handle.dyn_into::<web_sys::Clipboard>() {
|
||||||
|
Ok(clipboard) => {
|
||||||
|
let promise = clipboard.write_text(©_target);
|
||||||
|
if let Err(err) = JsFuture::from(promise).await {
|
||||||
|
log::warn!("Clipboard write failed: {:?}", err);
|
||||||
|
} else {
|
||||||
|
log::info!("Peer ID copied to clipboard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => log::warn!("Clipboard handle cast failed: {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
log::warn!("Clipboard API undefined on navigator");
|
||||||
|
}
|
||||||
|
Err(err) => log::warn!("Clipboard lookup failed: {:?}", err),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("Clipboard copy skipped: no window available");
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
"📋"
|
"📋"
|
||||||
}
|
}
|
||||||
@ -420,9 +121,9 @@ pub fn ConnectionPanel(
|
|||||||
class: "input",
|
class: "input",
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
placeholder: "Paste peer ID",
|
placeholder: "Paste peer ID",
|
||||||
value: "{remote_id.read()}",
|
value: "{remote_id_value}",
|
||||||
oninput: move |event| {
|
oninput: move |event| {
|
||||||
remote_id.set(event.value());
|
(set_remote_id_action.clone())(event.value());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,32 @@
|
|||||||
|
use crate::services::signaling::use_signaling;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn StatusDisplay(connected: Signal<bool>) -> Element {
|
pub fn StatusDisplay() -> Element {
|
||||||
|
let service = use_signaling();
|
||||||
|
let state = service.state.clone();
|
||||||
|
|
||||||
|
let connected = *state.connected.read();
|
||||||
|
let ws_status = state.ws_status.read().clone();
|
||||||
|
let in_call = *state.in_call.read();
|
||||||
|
let hint_text = format!(
|
||||||
|
"{} · {}",
|
||||||
|
ws_status,
|
||||||
|
if in_call {
|
||||||
|
"Active call"
|
||||||
|
} else {
|
||||||
|
"No active call"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "status-widget",
|
div { class: "status-widget",
|
||||||
span { class: "status-widget__label", "Signaling" }
|
span { class: "status-widget__label", "Signaling" }
|
||||||
span {
|
span {
|
||||||
class: if *connected.read() { "status-widget__value status-widget__value--online" } else { "status-widget__value status-widget__value--offline" },
|
class: if connected { "status-widget__value status-widget__value--online" } else { "status-widget__value status-widget__value--offline" },
|
||||||
if *connected.read() { "Online" } else { "Offline" }
|
if connected { "Online" } else { "Offline" }
|
||||||
}
|
}
|
||||||
span { class: "status-widget__hint", "TURN integration pending" }
|
span { class: "status-widget__hint", "{hint_text}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use crate::models::Participant;
|
use crate::models::Participant;
|
||||||
|
use crate::services::signaling::use_signaling;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use web_sys::{MediaStream, RtcPeerConnection, WebSocket as BrowserWebSocket};
|
|
||||||
|
|
||||||
use super::{CallControls, ConnectionPanel, StatusDisplay};
|
use super::{CallControls, ConnectionPanel, StatusDisplay};
|
||||||
|
|
||||||
@ -9,13 +11,6 @@ pub struct VoiceChannelProps {
|
|||||||
pub channel_name: String,
|
pub channel_name: String,
|
||||||
pub channel_topic: String,
|
pub channel_topic: String,
|
||||||
pub participants: Signal<Vec<Participant>>,
|
pub participants: Signal<Vec<Participant>>,
|
||||||
pub peer_id: Signal<String>,
|
|
||||||
pub remote_id: Signal<String>,
|
|
||||||
pub connected: Signal<bool>,
|
|
||||||
pub websocket: Signal<Option<BrowserWebSocket>>,
|
|
||||||
pub responder_connection: Signal<Option<RtcPeerConnection>>,
|
|
||||||
pub initiator_connection: Signal<Option<RtcPeerConnection>>,
|
|
||||||
pub local_media: Signal<Option<MediaStream>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@ -25,13 +20,6 @@ pub fn VoiceChannelLayout(props: VoiceChannelProps) -> Element {
|
|||||||
ChannelSidebar {
|
ChannelSidebar {
|
||||||
channel_name: props.channel_name.clone(),
|
channel_name: props.channel_name.clone(),
|
||||||
channel_topic: props.channel_topic.clone(),
|
channel_topic: props.channel_topic.clone(),
|
||||||
peer_id: props.peer_id.clone(),
|
|
||||||
remote_id: props.remote_id.clone(),
|
|
||||||
connected: props.connected.clone(),
|
|
||||||
websocket: props.websocket.clone(),
|
|
||||||
responder_connection: props.responder_connection.clone(),
|
|
||||||
initiator_connection: props.initiator_connection.clone(),
|
|
||||||
local_media: props.local_media.clone(),
|
|
||||||
}
|
}
|
||||||
div { class: "channel-main",
|
div { class: "channel-main",
|
||||||
ChannelHeader {
|
ChannelHeader {
|
||||||
@ -40,14 +28,7 @@ pub fn VoiceChannelLayout(props: VoiceChannelProps) -> Element {
|
|||||||
}
|
}
|
||||||
ParticipantsGrid { participants: props.participants.clone() }
|
ParticipantsGrid { participants: props.participants.clone() }
|
||||||
}
|
}
|
||||||
ControlDock {
|
ControlDock {}
|
||||||
peer_id: props.peer_id.clone(),
|
|
||||||
remote_id: props.remote_id.clone(),
|
|
||||||
connected: props.connected.clone(),
|
|
||||||
websocket: props.websocket.clone(),
|
|
||||||
initiator_connection: props.initiator_connection.clone(),
|
|
||||||
local_media: props.local_media.clone(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56,13 +37,6 @@ pub fn VoiceChannelLayout(props: VoiceChannelProps) -> Element {
|
|||||||
pub struct ChannelSidebarProps {
|
pub struct ChannelSidebarProps {
|
||||||
pub channel_name: String,
|
pub channel_name: String,
|
||||||
pub channel_topic: String,
|
pub channel_topic: String,
|
||||||
pub peer_id: Signal<String>,
|
|
||||||
pub remote_id: Signal<String>,
|
|
||||||
pub connected: Signal<bool>,
|
|
||||||
pub websocket: Signal<Option<BrowserWebSocket>>,
|
|
||||||
pub responder_connection: Signal<Option<RtcPeerConnection>>,
|
|
||||||
pub initiator_connection: Signal<Option<RtcPeerConnection>>,
|
|
||||||
pub local_media: Signal<Option<MediaStream>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@ -74,18 +48,10 @@ fn ChannelSidebar(props: ChannelSidebarProps) -> Element {
|
|||||||
p { "{props.channel_topic}" }
|
p { "{props.channel_topic}" }
|
||||||
}
|
}
|
||||||
div { class: "channel-sidebar__body",
|
div { class: "channel-sidebar__body",
|
||||||
ConnectionPanel {
|
ConnectionPanel {}
|
||||||
peer_id: props.peer_id.clone(),
|
|
||||||
remote_id: props.remote_id.clone(),
|
|
||||||
connected: props.connected.clone(),
|
|
||||||
websocket: props.websocket.clone(),
|
|
||||||
peer_connection: props.responder_connection.clone(),
|
|
||||||
initiator_connection: props.initiator_connection.clone(),
|
|
||||||
local_media: props.local_media.clone(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
div { class: "channel-sidebar__footer",
|
div { class: "channel-sidebar__footer",
|
||||||
StatusDisplay { connected: props.connected.clone() }
|
StatusDisplay {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,6 +65,28 @@ pub struct ChannelHeaderProps {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn ChannelHeader(props: ChannelHeaderProps) -> Element {
|
fn ChannelHeader(props: ChannelHeaderProps) -> Element {
|
||||||
|
let service = use_signaling();
|
||||||
|
let state = service.state.clone();
|
||||||
|
let connected = *state.connected.read();
|
||||||
|
let in_call = *state.in_call.read();
|
||||||
|
|
||||||
|
let call_label = if in_call { "In call" } else { "Call idle" };
|
||||||
|
let call_class = if in_call {
|
||||||
|
"channel-pill"
|
||||||
|
} else {
|
||||||
|
"channel-pill secondary"
|
||||||
|
};
|
||||||
|
let signaling_label = if connected {
|
||||||
|
"Signaling online"
|
||||||
|
} else {
|
||||||
|
"Signaling offline"
|
||||||
|
};
|
||||||
|
let signaling_class = if connected {
|
||||||
|
"channel-pill secondary"
|
||||||
|
} else {
|
||||||
|
"channel-pill secondary"
|
||||||
|
};
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
header { class: "channel-header",
|
header { class: "channel-header",
|
||||||
div { class: "channel-header__text",
|
div { class: "channel-header__text",
|
||||||
@ -106,8 +94,8 @@ fn ChannelHeader(props: ChannelHeaderProps) -> Element {
|
|||||||
p { "{props.topic}" }
|
p { "{props.topic}" }
|
||||||
}
|
}
|
||||||
div { class: "channel-header__actions",
|
div { class: "channel-header__actions",
|
||||||
button { class: "channel-pill", "Voice" }
|
button { class: "{call_class}", "{call_label}" }
|
||||||
button { class: "channel-pill secondary", "Active" }
|
button { class: "{signaling_class}", "{signaling_label}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,28 +169,11 @@ fn initials(name: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Props, Clone, PartialEq)]
|
|
||||||
pub struct ControlDockProps {
|
|
||||||
pub peer_id: Signal<String>,
|
|
||||||
pub remote_id: Signal<String>,
|
|
||||||
pub connected: Signal<bool>,
|
|
||||||
pub websocket: Signal<Option<BrowserWebSocket>>,
|
|
||||||
pub initiator_connection: Signal<Option<RtcPeerConnection>>,
|
|
||||||
pub local_media: Signal<Option<MediaStream>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn ControlDock(props: ControlDockProps) -> Element {
|
fn ControlDock() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
footer { class: "control-dock",
|
footer { class: "control-dock",
|
||||||
CallControls {
|
CallControls {}
|
||||||
peer_id: props.peer_id.clone(),
|
|
||||||
remote_id: props.remote_id.clone(),
|
|
||||||
connected: props.connected.clone(),
|
|
||||||
websocket: props.websocket.clone(),
|
|
||||||
peer_connection: props.initiator_connection.clone(),
|
|
||||||
local_media: props.local_media.clone(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ pub mod components;
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
|
pub mod services;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
// Re-export commonly used items if needed in the future
|
// Re-export commonly used items if needed in the future
|
||||||
|
|||||||
27
src/main.rs
27
src/main.rs
@ -5,7 +5,7 @@ use dioxus::prelude::*;
|
|||||||
use log::Level;
|
use log::Level;
|
||||||
use niom_webrtc::components::VoiceChannelLayout;
|
use niom_webrtc::components::VoiceChannelLayout;
|
||||||
use niom_webrtc::models::Participant;
|
use niom_webrtc::models::Participant;
|
||||||
use web_sys::{MediaStream, RtcPeerConnection, WebSocket as BrowserWebSocket};
|
use niom_webrtc::services::signaling::SignalingProvider;
|
||||||
// config functions used via fully-qualified paths below
|
// config functions used via fully-qualified paths below
|
||||||
|
|
||||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||||
@ -59,14 +59,6 @@ fn ConfigProvider() -> Element {
|
|||||||
pub fn Content() -> Element {
|
pub fn Content() -> Element {
|
||||||
// Config is provided by ConfigProvider via provide_context; components can use use_context to read it.
|
// Config is provided by ConfigProvider via provide_context; components can use use_context to read it.
|
||||||
|
|
||||||
let peer_id = use_signal(|| "peer-loading...".to_string());
|
|
||||||
let remote_id = use_signal(|| String::new());
|
|
||||||
let connected = use_signal(|| false);
|
|
||||||
let websocket = use_signal(|| None::<BrowserWebSocket>);
|
|
||||||
let initiator_connection = use_signal(|| None::<RtcPeerConnection>);
|
|
||||||
let responder_connection = use_signal(|| None::<RtcPeerConnection>);
|
|
||||||
let local_media = use_signal(|| None::<MediaStream>);
|
|
||||||
|
|
||||||
let participants = use_signal(|| {
|
let participants = use_signal(|| {
|
||||||
vec![
|
vec![
|
||||||
Participant::new("self", "Ghost", "#5865F2", true, false, true),
|
Participant::new("self", "Ghost", "#5865F2", true, false, true),
|
||||||
@ -79,17 +71,12 @@ pub fn Content() -> Element {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
VoiceChannelLayout {
|
SignalingProvider {
|
||||||
channel_name: "Project Alpha / Voice Lounge".to_string(),
|
VoiceChannelLayout {
|
||||||
channel_topic: "Team sync & architecture deep dive".to_string(),
|
channel_name: "Project Alpha / Voice Lounge".to_string(),
|
||||||
participants,
|
channel_topic: "Team sync & architecture deep dive".to_string(),
|
||||||
peer_id,
|
participants,
|
||||||
remote_id,
|
}
|
||||||
connected,
|
|
||||||
websocket,
|
|
||||||
responder_connection,
|
|
||||||
initiator_connection,
|
|
||||||
local_media,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/services/mod.rs
Normal file
1
src/services/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod signaling;
|
||||||
677
src/services/signaling.rs
Normal file
677
src/services/signaling.rs
Normal file
@ -0,0 +1,677 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config, constants::DEFAULT_SIGNALING_URL, models::SignalingMessage, utils::MediaManager,
|
||||||
|
};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use web_sys::{
|
||||||
|
BinaryType, MediaStream, MessageEvent, RtcPeerConnection, WebSocket as BrowserWebSocket,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SignalingState {
|
||||||
|
pub peer_id: Signal<String>,
|
||||||
|
pub remote_id: Signal<String>,
|
||||||
|
pub ws_status: Signal<String>,
|
||||||
|
pub connected: Signal<bool>,
|
||||||
|
pub websocket: Signal<Option<BrowserWebSocket>>,
|
||||||
|
pub initiator_connection: Signal<Option<RtcPeerConnection>>,
|
||||||
|
pub responder_connection: Signal<Option<RtcPeerConnection>>,
|
||||||
|
pub local_media: Signal<Option<MediaStream>>,
|
||||||
|
pub pending_answer: Signal<Option<String>>,
|
||||||
|
pub mic_granted: Signal<bool>,
|
||||||
|
pub in_call: Signal<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SignalingActions {
|
||||||
|
pub connect: Rc<dyn Fn()>,
|
||||||
|
pub set_remote_id: Rc<dyn Fn(String)>,
|
||||||
|
pub request_microphone: Rc<dyn Fn()>,
|
||||||
|
pub start_call: Rc<dyn Fn()>,
|
||||||
|
pub leave_call: Rc<dyn Fn()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SignalingService {
|
||||||
|
pub state: SignalingState,
|
||||||
|
pub actions: SignalingActions,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_signaling() -> SignalingService {
|
||||||
|
use_context::<SignalingService>()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props, Clone, PartialEq)]
|
||||||
|
pub struct SignalingProviderProps {
|
||||||
|
pub children: Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SignalingProvider(props: SignalingProviderProps) -> Element {
|
||||||
|
let peer_id = use_signal(|| String::new());
|
||||||
|
let remote_id = use_signal(|| String::new());
|
||||||
|
let ws_status = use_signal(|| "Nicht verbunden".to_string());
|
||||||
|
let connected = use_signal(|| false);
|
||||||
|
let websocket = use_signal(|| None::<BrowserWebSocket>);
|
||||||
|
let initiator_connection = use_signal(|| None::<RtcPeerConnection>);
|
||||||
|
let responder_connection = use_signal(|| None::<RtcPeerConnection>);
|
||||||
|
let local_media = use_signal(|| None::<MediaStream>);
|
||||||
|
let pending_answer = use_signal(|| None::<String>);
|
||||||
|
let mic_granted = use_signal(|| false);
|
||||||
|
let in_call = use_signal(|| false);
|
||||||
|
|
||||||
|
let cfg_signal: Signal<Config> = use_context();
|
||||||
|
|
||||||
|
let offer_handler = {
|
||||||
|
let websocket = websocket.clone();
|
||||||
|
let peer_connection = responder_connection.clone();
|
||||||
|
let peer_id = peer_id.clone();
|
||||||
|
let pending_answer_signal = pending_answer.clone();
|
||||||
|
let initiator_connection = initiator_connection.clone();
|
||||||
|
use_coroutine(move |mut rx| async move {
|
||||||
|
while let Some(msg) = rx.next().await {
|
||||||
|
let SignalingMessage {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
msg_type,
|
||||||
|
data,
|
||||||
|
} = msg;
|
||||||
|
|
||||||
|
if msg_type == "offer" {
|
||||||
|
log::info!("📞 WebRTC-Offer von {} als Responder verarbeiten", from);
|
||||||
|
let from_clone = from.clone();
|
||||||
|
let from_for_log = from.clone();
|
||||||
|
|
||||||
|
let pc = if peer_connection.read().is_none() {
|
||||||
|
match MediaManager::create_peer_connection() {
|
||||||
|
Ok(new_pc) => {
|
||||||
|
let ws_clone = websocket.clone();
|
||||||
|
let peer_id_clone = peer_id.clone();
|
||||||
|
let on_ice = Closure::wrap(Box::new(move |ev: JsValue| {
|
||||||
|
if let Ok(candidate_val) =
|
||||||
|
js_sys::Reflect::get(&ev, &JsValue::from_str("candidate"))
|
||||||
|
{
|
||||||
|
if candidate_val.is_null() || candidate_val.is_undefined() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(ws) = ws_clone.read().as_ref() {
|
||||||
|
if let Ok(json_js) =
|
||||||
|
js_sys::JSON::stringify(&candidate_val)
|
||||||
|
{
|
||||||
|
if let Some(json) = json_js.as_string() {
|
||||||
|
let msg = SignalingMessage {
|
||||||
|
from: peer_id_clone.read().clone(),
|
||||||
|
to: from_clone.clone(),
|
||||||
|
msg_type: "candidate".to_string(),
|
||||||
|
data: json,
|
||||||
|
};
|
||||||
|
if let Ok(text) = serde_json::to_string(&msg) {
|
||||||
|
let _ = ws.send_with_str(&text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
as Box<dyn FnMut(JsValue)>);
|
||||||
|
new_pc.set_onicecandidate(Some(on_ice.as_ref().unchecked_ref()));
|
||||||
|
on_ice.forget();
|
||||||
|
|
||||||
|
let on_track = Closure::wrap(Box::new(move |ev: JsValue| {
|
||||||
|
if let Ok(streams_val) =
|
||||||
|
js_sys::Reflect::get(&ev, &JsValue::from_str("streams"))
|
||||||
|
{
|
||||||
|
if streams_val.is_undefined() || streams_val.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let streams_array = js_sys::Array::from(&streams_val);
|
||||||
|
let first = streams_array.get(0);
|
||||||
|
let stream_js = first.clone();
|
||||||
|
if let Ok(stream) =
|
||||||
|
stream_js.dyn_into::<web_sys::MediaStream>()
|
||||||
|
{
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
if let Some(document) = window.document() {
|
||||||
|
if let Ok(audio_el) =
|
||||||
|
document.create_element("audio")
|
||||||
|
{
|
||||||
|
if let Ok(audio) = audio_el
|
||||||
|
.dyn_into::<web_sys::HtmlAudioElement>(
|
||||||
|
) {
|
||||||
|
audio.set_autoplay(true);
|
||||||
|
audio.set_src_object(Some(&stream));
|
||||||
|
if let Some(body) = document.body() {
|
||||||
|
let _ =
|
||||||
|
body.append_child(&audio).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
as Box<dyn FnMut(JsValue)>);
|
||||||
|
new_pc.set_ontrack(Some(on_track.as_ref().unchecked_ref()));
|
||||||
|
on_track.forget();
|
||||||
|
|
||||||
|
let mut responder_handle = peer_connection.clone();
|
||||||
|
responder_handle.set(Some(new_pc.clone()));
|
||||||
|
log::info!(
|
||||||
|
"✅ Responder PeerConnection für {} erstellt",
|
||||||
|
from_for_log
|
||||||
|
);
|
||||||
|
new_pc
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("❌ Responder PeerConnection-Fehler: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
peer_connection.read().as_ref().unwrap().clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
match MediaManager::handle_offer(&pc, &data).await {
|
||||||
|
Ok(answer_sdp) => {
|
||||||
|
log::info!("✅ Responder Answer erstellt, sende zurück...");
|
||||||
|
|
||||||
|
if let Some(socket) = websocket.read().as_ref() {
|
||||||
|
let answer_msg = SignalingMessage {
|
||||||
|
from: to,
|
||||||
|
to: from,
|
||||||
|
msg_type: "answer".to_string(),
|
||||||
|
data: answer_sdp,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(json) = serde_json::to_string(&answer_msg) {
|
||||||
|
let _ = socket.send_with_str(&json);
|
||||||
|
log::info!("📤 Responder Answer gesendet an {}", from_for_log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("❌ Responder Answer-Fehler: {}", e),
|
||||||
|
}
|
||||||
|
} else if msg_type == "answer" {
|
||||||
|
log::info!("🔀 Answer empfangen - leite an Initiator-PeerConnection weiter");
|
||||||
|
let data_clone = data.clone();
|
||||||
|
if let Some(pc) = initiator_connection.read().as_ref() {
|
||||||
|
let pc_clone = pc.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
match 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 {
|
||||||
|
log::warn!("⚠️ Keine Initiator-PeerConnection vorhanden - buffer Answer");
|
||||||
|
let mut pending_handle = pending_answer_signal.clone();
|
||||||
|
pending_handle.set(Some(data_clone));
|
||||||
|
}
|
||||||
|
} else if msg_type == "candidate" {
|
||||||
|
let data_clone = data.clone();
|
||||||
|
if let Some(pc) = initiator_connection.read().as_ref() {
|
||||||
|
let pc_clone = pc.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
match 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 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");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::info!("❓ Unbekannte Nachricht: {}", msg_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
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 cfg_signal_handle = cfg_signal.clone();
|
||||||
|
let initiator_connection_signal = initiator_connection.clone();
|
||||||
|
let responder_connection_signal = responder_connection.clone();
|
||||||
|
|
||||||
|
Rc::new(move || {
|
||||||
|
let mut ws_status_handle = ws_status_signal.clone();
|
||||||
|
let connected_handle = connected_signal.clone();
|
||||||
|
let mut websocket_handle = websocket_signal.clone();
|
||||||
|
let initiator_connection = initiator_connection_signal.clone();
|
||||||
|
let responder_connection = responder_connection_signal.clone();
|
||||||
|
let cfg_signal = cfg_signal_handle.clone();
|
||||||
|
|
||||||
|
if *connected_handle.read() || websocket_handle.read().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws_status_handle.set("Verbinde...".to_string());
|
||||||
|
|
||||||
|
let endpoint = cfg_signal.read().server.signaling_url.trim().to_string();
|
||||||
|
let target = if endpoint.is_empty() {
|
||||||
|
DEFAULT_SIGNALING_URL.to_string()
|
||||||
|
} else {
|
||||||
|
endpoint
|
||||||
|
};
|
||||||
|
|
||||||
|
log::info!("🔌 Verbinde WebSocket zu {}", target);
|
||||||
|
|
||||||
|
match BrowserWebSocket::new(&target) {
|
||||||
|
Ok(socket) => {
|
||||||
|
socket.set_binary_type(BinaryType::Arraybuffer);
|
||||||
|
|
||||||
|
let mut ws_status_clone = ws_status_handle.clone();
|
||||||
|
let mut connected_clone = connected_handle.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)>);
|
||||||
|
|
||||||
|
let mut ws_status_clone2 = ws_status_handle.clone();
|
||||||
|
let mut connected_clone2 = connected_handle.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)>);
|
||||||
|
|
||||||
|
let offer_tx = offer_handler.clone();
|
||||||
|
let initiator_for_router = initiator_connection.clone();
|
||||||
|
let responder_for_router = responder_connection.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" => offer_tx.send(msg),
|
||||||
|
"answer" => {
|
||||||
|
let data_clone = msg.data.clone();
|
||||||
|
if let Some(pc) = initiator_for_router.read().as_ref() {
|
||||||
|
let pc_clone = pc.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
match 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 {
|
||||||
|
log::warn!("⚠️ Keine Initiator-PeerConnection vorhanden - buffer Answer");
|
||||||
|
let mut pending_handle = pending_answer_signal.clone();
|
||||||
|
pending_handle.set(Some(data_clone));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"candidate" => {
|
||||||
|
let data_clone = msg.data.clone();
|
||||||
|
if let Some(pc) = initiator_for_router.read().as_ref() {
|
||||||
|
let pc_clone = pc.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
match 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) =
|
||||||
|
responder_for_router.read().as_ref()
|
||||||
|
{
|
||||||
|
let pc_clone = pc.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
match 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" => {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
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()));
|
||||||
|
|
||||||
|
onopen.forget();
|
||||||
|
onclose.forget();
|
||||||
|
onmessage.forget();
|
||||||
|
|
||||||
|
websocket_handle.set(Some(socket));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("❌ WebSocket Fehler: {:?}", e);
|
||||||
|
ws_status_handle.set("Verbindungsfehler".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
use_effect({
|
||||||
|
let peer_id = peer_id.clone();
|
||||||
|
move || {
|
||||||
|
use js_sys::{Date, Math};
|
||||||
|
let timestamp = Date::now() as u64;
|
||||||
|
let random = (Math::random() * 900.0 + 100.0) as u32;
|
||||||
|
let id = format!("peer-{}-{}", timestamp, random);
|
||||||
|
let mut peer_id_handle = peer_id.clone();
|
||||||
|
peer_id_handle.set(id.clone());
|
||||||
|
log::info!("🆔 Peer-ID generiert: {}", id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
use_effect({
|
||||||
|
let connect_logic = connect_logic.clone();
|
||||||
|
move || {
|
||||||
|
connect_logic();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
let pending = pending_answer.clone();
|
||||||
|
let initiator_connection = initiator_connection.clone();
|
||||||
|
use_effect(move || {
|
||||||
|
if let Some(answer_sdp) = pending.read().clone() {
|
||||||
|
if let Some(pc) = initiator_connection.read().as_ref() {
|
||||||
|
let pc_clone = pc.clone();
|
||||||
|
let answer_clone = answer_sdp.clone();
|
||||||
|
let mut pending_signal = pending.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
match MediaManager::handle_answer(&pc_clone, &answer_clone).await {
|
||||||
|
Ok(_) => log::info!(
|
||||||
|
"✅ Gepufferte Answer erfolgreich gesetzt auf Initiator-PC"
|
||||||
|
),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("❌ Fehler beim Setzen der gepufferten Answer: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pending_signal.set(None);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let set_remote_id = {
|
||||||
|
let remote_id = remote_id.clone();
|
||||||
|
Rc::new(move |value: String| {
|
||||||
|
let mut handle = remote_id.clone();
|
||||||
|
let sanitized = value.trim().to_string();
|
||||||
|
handle.set(sanitized);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_microphone = {
|
||||||
|
let mic_granted = mic_granted.clone();
|
||||||
|
let local_media = local_media.clone();
|
||||||
|
let initiator_connection = initiator_connection.clone();
|
||||||
|
Rc::new(move || {
|
||||||
|
let mut mic_granted_signal = mic_granted.clone();
|
||||||
|
let mut local_media_signal = local_media.clone();
|
||||||
|
let initiator_connection_signal = initiator_connection.clone();
|
||||||
|
spawn(async move {
|
||||||
|
let mut manager = MediaManager::new();
|
||||||
|
match manager.request_microphone_access().await {
|
||||||
|
Ok(stream) => {
|
||||||
|
log::info!("Microphone granted");
|
||||||
|
mic_granted_signal.set(true);
|
||||||
|
local_media_signal.set(Some(stream.clone()));
|
||||||
|
if let Some(pc) = initiator_connection_signal.read().as_ref() {
|
||||||
|
if let Err(e) = MediaManager::add_stream_to_pc(pc, &stream) {
|
||||||
|
log::warn!("Failed to attach local tracks: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Microphone request failed: {}", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let start_call = {
|
||||||
|
let initiator_connection = initiator_connection.clone();
|
||||||
|
let websocket = websocket.clone();
|
||||||
|
let peer_id = peer_id.clone();
|
||||||
|
let remote_id = remote_id.clone();
|
||||||
|
let local_media = local_media.clone();
|
||||||
|
let mic_granted = mic_granted.clone();
|
||||||
|
let in_call = in_call.clone();
|
||||||
|
Rc::new(move || {
|
||||||
|
let initiator_signal = initiator_connection.clone();
|
||||||
|
let websocket_signal = websocket.clone();
|
||||||
|
let peer_id_signal = peer_id.clone();
|
||||||
|
let remote_id_signal = remote_id.clone();
|
||||||
|
let local_media_signal = local_media.clone();
|
||||||
|
let mic_granted_signal = mic_granted.clone();
|
||||||
|
let in_call_signal = in_call.clone();
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
if *in_call_signal.read() {
|
||||||
|
log::info!("Call already active; ignoring start request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !*mic_granted_signal.read() {
|
||||||
|
log::warn!("Mic not granted, cannot start call");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pc = if initiator_signal.read().is_none() {
|
||||||
|
match MediaManager::create_peer_connection() {
|
||||||
|
Ok(new_pc) => {
|
||||||
|
let ws_clone = websocket_signal.clone();
|
||||||
|
let to_clone = remote_id_signal.read().clone();
|
||||||
|
let from_clone = peer_id_signal.read().clone();
|
||||||
|
let on_ice = Closure::wrap(Box::new(move |ev: JsValue| {
|
||||||
|
if let Ok(candidate_val) =
|
||||||
|
js_sys::Reflect::get(&ev, &JsValue::from_str("candidate"))
|
||||||
|
{
|
||||||
|
if candidate_val.is_null() || candidate_val.is_undefined() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(ws) = ws_clone.read().as_ref() {
|
||||||
|
if let Ok(json_js) = js_sys::JSON::stringify(&candidate_val)
|
||||||
|
{
|
||||||
|
if let Some(json) = json_js.as_string() {
|
||||||
|
let msg = SignalingMessage {
|
||||||
|
from: from_clone.clone(),
|
||||||
|
to: to_clone.clone(),
|
||||||
|
msg_type: "candidate".to_string(),
|
||||||
|
data: json,
|
||||||
|
};
|
||||||
|
if let Ok(text) = serde_json::to_string(&msg) {
|
||||||
|
let _ = ws.send_with_str(&text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
as Box<dyn FnMut(JsValue)>);
|
||||||
|
new_pc.set_onicecandidate(Some(on_ice.as_ref().unchecked_ref()));
|
||||||
|
on_ice.forget();
|
||||||
|
|
||||||
|
let on_track = Closure::wrap(Box::new(move |ev: JsValue| {
|
||||||
|
if let Ok(streams_val) =
|
||||||
|
js_sys::Reflect::get(&ev, &JsValue::from_str("streams"))
|
||||||
|
{
|
||||||
|
if streams_val.is_undefined() || streams_val.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let streams_array = js_sys::Array::from(&streams_val);
|
||||||
|
let first = streams_array.get(0);
|
||||||
|
if let Ok(stream) =
|
||||||
|
first.clone().dyn_into::<web_sys::MediaStream>()
|
||||||
|
{
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
if let Some(document) = window.document() {
|
||||||
|
if let Ok(audio_el) =
|
||||||
|
document.create_element("audio")
|
||||||
|
{
|
||||||
|
if let Ok(audio) = audio_el
|
||||||
|
.dyn_into::<web_sys::HtmlAudioElement>(
|
||||||
|
) {
|
||||||
|
audio.set_autoplay(true);
|
||||||
|
audio.set_src_object(Some(&stream));
|
||||||
|
if let Some(body) = document.body() {
|
||||||
|
let _ = body.append_child(&audio).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
as Box<dyn FnMut(JsValue)>);
|
||||||
|
new_pc.set_ontrack(Some(on_track.as_ref().unchecked_ref()));
|
||||||
|
on_track.forget();
|
||||||
|
|
||||||
|
let mut initiator_writer = initiator_signal.clone();
|
||||||
|
initiator_writer.set(Some(new_pc.clone()));
|
||||||
|
log::info!("Initiator PeerConnection ready");
|
||||||
|
new_pc
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to create initiator peer connection: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initiator_signal.read().as_ref().unwrap().clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(local) = local_media_signal.read().as_ref() {
|
||||||
|
if let Err(e) = MediaManager::add_stream_to_pc(&pc, local) {
|
||||||
|
log::warn!("Failed to attach local tracks before offer: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match MediaManager::create_offer(&pc).await {
|
||||||
|
Ok(offer_sdp) => {
|
||||||
|
if let Some(socket) = websocket_signal.read().as_ref() {
|
||||||
|
let msg = SignalingMessage {
|
||||||
|
from: peer_id_signal.read().clone(),
|
||||||
|
to: remote_id_signal.read().clone(),
|
||||||
|
msg_type: "offer".to_string(),
|
||||||
|
data: offer_sdp,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(json) = serde_json::to_string(&msg) {
|
||||||
|
let _ = socket.send_with_str(&json);
|
||||||
|
log::info!(
|
||||||
|
"Offer dispatched to {}",
|
||||||
|
remote_id_signal.read().as_str()
|
||||||
|
);
|
||||||
|
let mut in_call_writer = in_call_signal.clone();
|
||||||
|
in_call_writer.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Offer creation failed: {}", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let leave_call = {
|
||||||
|
let initiator_connection = initiator_connection.clone();
|
||||||
|
let in_call = in_call.clone();
|
||||||
|
let mic_granted = mic_granted.clone();
|
||||||
|
let local_media = local_media.clone();
|
||||||
|
Rc::new(move || {
|
||||||
|
let mut initiator_signal = initiator_connection.clone();
|
||||||
|
let mut in_call_signal = in_call.clone();
|
||||||
|
let mut local_media_signal = local_media.clone();
|
||||||
|
let mut mic_granted_signal = mic_granted.clone();
|
||||||
|
|
||||||
|
if *in_call_signal.read() {
|
||||||
|
if let Some(pc) = initiator_signal.read().as_ref() {
|
||||||
|
pc.close();
|
||||||
|
log::info!("Initiator PeerConnection closed");
|
||||||
|
}
|
||||||
|
initiator_signal.set(None);
|
||||||
|
}
|
||||||
|
let current_stream = local_media_signal.read().as_ref().cloned();
|
||||||
|
if let Some(stream) = current_stream {
|
||||||
|
let tracks = stream.get_tracks();
|
||||||
|
for i in 0..tracks.length() {
|
||||||
|
if let Ok(track) = tracks.get(i).dyn_into::<web_sys::MediaStreamTrack>() {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
local_media_signal.set(None);
|
||||||
|
}
|
||||||
|
mic_granted_signal.set(false);
|
||||||
|
in_call_signal.set(false);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let service = SignalingService {
|
||||||
|
state: SignalingState {
|
||||||
|
peer_id: peer_id.clone(),
|
||||||
|
remote_id: remote_id.clone(),
|
||||||
|
ws_status: ws_status.clone(),
|
||||||
|
connected: connected.clone(),
|
||||||
|
websocket: websocket.clone(),
|
||||||
|
initiator_connection: initiator_connection.clone(),
|
||||||
|
responder_connection: responder_connection.clone(),
|
||||||
|
local_media: local_media.clone(),
|
||||||
|
pending_answer: pending_answer.clone(),
|
||||||
|
mic_granted: mic_granted.clone(),
|
||||||
|
in_call: in_call.clone(),
|
||||||
|
},
|
||||||
|
actions: SignalingActions {
|
||||||
|
connect: connect_logic.clone(),
|
||||||
|
set_remote_id,
|
||||||
|
request_microphone,
|
||||||
|
start_call,
|
||||||
|
leave_call,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use_context_provider(|| service.clone());
|
||||||
|
|
||||||
|
props.children
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user