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",
|
||||
"ErrorEvent",
|
||||
"Navigator",
|
||||
"Clipboard",
|
||||
"MediaDevices",
|
||||
"MediaStream",
|
||||
"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::utils::MediaManager;
|
||||
use crate::services::signaling::use_signaling;
|
||||
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]
|
||||
pub fn CallControls(
|
||||
peer_id: Signal<String>,
|
||||
remote_id: Signal<String>,
|
||||
connected: Signal<bool>,
|
||||
websocket: Signal<Option<BrowserWebSocket>>,
|
||||
peer_connection: Signal<Option<RtcPeerConnection>>, // **INITIATOR CONNECTION**
|
||||
local_media: Signal<Option<MediaStream>>,
|
||||
) -> Element {
|
||||
let mic_granted = use_signal(|| false);
|
||||
pub fn CallControls() -> Element {
|
||||
let service = use_signaling();
|
||||
let state = service.state.clone();
|
||||
let actions = service.actions.clone();
|
||||
|
||||
let peer_id_value = state.peer_id.read().clone();
|
||||
let remote_id_value = state.remote_id.read().clone();
|
||||
let remote_id_for_label = remote_id_value.clone();
|
||||
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 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! {
|
||||
div { class: "call-controls",
|
||||
div { class: "call-controls__left",
|
||||
span { class: "self-pill", "Your ID: {peer_id.read()}" }
|
||||
if !remote_id.read().is_empty() {
|
||||
span { class: "self-pill self-pill--target", "Target: {remote_id.read()}" }
|
||||
span { class: "self-pill", "Your ID: {peer_id_value}" }
|
||||
if !remote_id_for_label.is_empty() {
|
||||
span { class: "self-pill self-pill--target", "Target: {remote_id_for_label}" }
|
||||
}
|
||||
}
|
||||
div { class: "call-controls__center",
|
||||
button {
|
||||
class: if *mic_granted.read() { "ctrl-btn ctrl-btn--secondary" } else { "ctrl-btn ctrl-btn--primary" },
|
||||
disabled: *mic_granted.read(),
|
||||
class: if mic_ready { "ctrl-btn ctrl-btn--secondary" } else { "ctrl-btn ctrl-btn--primary" },
|
||||
disabled: mic_ready,
|
||||
onclick: move |_| {
|
||||
log::info!("Requesting microphone permission");
|
||||
let mut mm_state = mic_granted.clone();
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
request_microphone();
|
||||
},
|
||||
if *mic_granted.read() { "Mic ready" } else { "Enable mic" }
|
||||
if mic_ready { "Mic ready" } else { "Enable mic" }
|
||||
}
|
||||
button {
|
||||
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 |_| {
|
||||
if in_call {
|
||||
return;
|
||||
}
|
||||
log::info!("Launching WebRTC call as initiator");
|
||||
|
||||
let mut pc_signal = peer_connection.clone();
|
||||
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),
|
||||
}
|
||||
});
|
||||
audio_muted.set(false);
|
||||
start_call();
|
||||
},
|
||||
"Start call"
|
||||
if in_call { "In call" } else { "Start call" }
|
||||
}
|
||||
button {
|
||||
class: if *audio_muted.read() { "ctrl-btn ctrl-btn--muted" } else { "ctrl-btn" },
|
||||
disabled: !*in_call.read(),
|
||||
class: if muted { "ctrl-btn ctrl-btn--muted" } else { "ctrl-btn" },
|
||||
disabled: !in_call,
|
||||
onclick: move |_| {
|
||||
let current_muted = *audio_muted.read();
|
||||
audio_muted.set(!current_muted);
|
||||
log::info!("Audio {}", if !current_muted { "muted" } else { "unmuted" });
|
||||
let current = *mute_signal.read();
|
||||
mute_signal.set(!current);
|
||||
log::info!("Audio {}", if current { "unmuted" } else { "muted" });
|
||||
},
|
||||
if *audio_muted.read() { "Unmute" } else { "Mute" }
|
||||
if muted { "Unmute" } else { "Mute" }
|
||||
}
|
||||
button {
|
||||
class: "ctrl-btn ctrl-btn--danger",
|
||||
disabled: !*in_call.read(),
|
||||
disabled: !in_call,
|
||||
onclick: move |_| {
|
||||
in_call.set(false);
|
||||
audio_muted.set(false);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
reset_signal.set(false);
|
||||
leave_call();
|
||||
log::info!("Call ended");
|
||||
},
|
||||
"Leave"
|
||||
@ -198,7 +78,13 @@ pub fn CallControls(
|
||||
}
|
||||
div { class: "call-controls__right",
|
||||
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::models::SignalingMessage;
|
||||
use crate::utils::MediaManager;
|
||||
use crate::services::signaling::use_signaling;
|
||||
use dioxus::prelude::*;
|
||||
use futures::StreamExt;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::prelude::Closure;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::MediaStream;
|
||||
use web_sys::{BinaryType, MessageEvent, RtcPeerConnection, WebSocket as BrowserWebSocket};
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
|
||||
#[component]
|
||||
pub fn ConnectionPanel(
|
||||
mut peer_id: Signal<String>,
|
||||
mut remote_id: Signal<String>,
|
||||
mut connected: Signal<bool>,
|
||||
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();
|
||||
pub fn ConnectionPanel() -> Element {
|
||||
let service = use_signaling();
|
||||
let state = service.state.clone();
|
||||
let actions = service.actions.clone();
|
||||
|
||||
// **COROUTINE** für Offer-Handling (Responder empfängt Offers)
|
||||
let offer_handler = use_coroutine(move |mut rx| async move {
|
||||
while let Some(msg) = rx.next().await {
|
||||
let SignalingMessage {
|
||||
from,
|
||||
to,
|
||||
msg_type,
|
||||
data,
|
||||
} = msg;
|
||||
let connected = *state.connected.read();
|
||||
let ws_status = state.ws_status.read().clone();
|
||||
let peer_id_value = state.peer_id.read().clone();
|
||||
let peer_id_for_copy = peer_id_value.clone();
|
||||
let remote_id_value = state.remote_id.read().clone();
|
||||
let mic_ready = *state.mic_granted.read();
|
||||
let in_call = *state.in_call.read();
|
||||
|
||||
// **KORREKT:** In der Coroutine-Loop
|
||||
if msg_type == "offer" {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let reconnect = actions.connect.clone();
|
||||
let set_remote_id_action = actions.set_remote_id.clone();
|
||||
|
||||
rsx! {
|
||||
div { class: "connection-panel",
|
||||
section { class: "connection-card",
|
||||
header { class: "connection-card__header",
|
||||
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",
|
||||
li {
|
||||
span { class: "label", "WebSocket" }
|
||||
span {
|
||||
class: if *connected.read() { "value value--success" } else { "value value--danger" },
|
||||
if *connected.read() { "Connected" } else { "Disconnected" }
|
||||
class: if connected { "value value--success" } else { "value value--danger" },
|
||||
if connected { "Connected" } else { "Disconnected" }
|
||||
}
|
||||
}
|
||||
li {
|
||||
span { class: "label", "Microphone" }
|
||||
span {
|
||||
class: if local_media.read().is_some() { "value value--success" } else { "value value--danger" },
|
||||
"{mic_status}"
|
||||
class: if mic_ready { "value value--success" } else { "value value--danger" },
|
||||
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 {
|
||||
class: "input input--readonly",
|
||||
r#type: "text",
|
||||
value: "{peer_id.read()}",
|
||||
value: "{peer_id_value}",
|
||||
readonly: true
|
||||
}
|
||||
button {
|
||||
class: "icon-btn",
|
||||
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",
|
||||
r#type: "text",
|
||||
placeholder: "Paste peer ID",
|
||||
value: "{remote_id.read()}",
|
||||
value: "{remote_id_value}",
|
||||
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::*;
|
||||
|
||||
#[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! {
|
||||
div { class: "status-widget",
|
||||
span { class: "status-widget__label", "Signaling" }
|
||||
span {
|
||||
class: if *connected.read() { "status-widget__value status-widget__value--online" } else { "status-widget__value status-widget__value--offline" },
|
||||
if *connected.read() { "Online" } else { "Offline" }
|
||||
class: if connected { "status-widget__value status-widget__value--online" } else { "status-widget__value status-widget__value--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::services::signaling::use_signaling;
|
||||
use dioxus::prelude::*;
|
||||
use web_sys::{MediaStream, RtcPeerConnection, WebSocket as BrowserWebSocket};
|
||||
|
||||
use super::{CallControls, ConnectionPanel, StatusDisplay};
|
||||
|
||||
@ -9,13 +11,6 @@ pub struct VoiceChannelProps {
|
||||
pub channel_name: String,
|
||||
pub channel_topic: String,
|
||||
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]
|
||||
@ -25,13 +20,6 @@ pub fn VoiceChannelLayout(props: VoiceChannelProps) -> Element {
|
||||
ChannelSidebar {
|
||||
channel_name: props.channel_name.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",
|
||||
ChannelHeader {
|
||||
@ -40,14 +28,7 @@ pub fn VoiceChannelLayout(props: VoiceChannelProps) -> Element {
|
||||
}
|
||||
ParticipantsGrid { participants: props.participants.clone() }
|
||||
}
|
||||
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(),
|
||||
}
|
||||
ControlDock {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,13 +37,6 @@ pub fn VoiceChannelLayout(props: VoiceChannelProps) -> Element {
|
||||
pub struct ChannelSidebarProps {
|
||||
pub channel_name: 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]
|
||||
@ -74,18 +48,10 @@ fn ChannelSidebar(props: ChannelSidebarProps) -> Element {
|
||||
p { "{props.channel_topic}" }
|
||||
}
|
||||
div { class: "channel-sidebar__body",
|
||||
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(),
|
||||
}
|
||||
ConnectionPanel {}
|
||||
}
|
||||
div { class: "channel-sidebar__footer",
|
||||
StatusDisplay { connected: props.connected.clone() }
|
||||
StatusDisplay {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -99,6 +65,28 @@ pub struct ChannelHeaderProps {
|
||||
|
||||
#[component]
|
||||
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! {
|
||||
header { class: "channel-header",
|
||||
div { class: "channel-header__text",
|
||||
@ -106,8 +94,8 @@ fn ChannelHeader(props: ChannelHeaderProps) -> Element {
|
||||
p { "{props.topic}" }
|
||||
}
|
||||
div { class: "channel-header__actions",
|
||||
button { class: "channel-pill", "Voice" }
|
||||
button { class: "channel-pill secondary", "Active" }
|
||||
button { class: "{call_class}", "{call_label}" }
|
||||
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]
|
||||
fn ControlDock(props: ControlDockProps) -> Element {
|
||||
fn ControlDock() -> Element {
|
||||
rsx! {
|
||||
footer { class: "control-dock",
|
||||
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(),
|
||||
}
|
||||
CallControls {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ pub mod components;
|
||||
pub mod config;
|
||||
pub mod constants;
|
||||
pub mod models;
|
||||
pub mod services;
|
||||
pub mod utils;
|
||||
|
||||
// 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 niom_webrtc::components::VoiceChannelLayout;
|
||||
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
|
||||
|
||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||
@ -59,14 +59,6 @@ fn ConfigProvider() -> Element {
|
||||
pub fn Content() -> Element {
|
||||
// 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(|| {
|
||||
vec![
|
||||
Participant::new("self", "Ghost", "#5865F2", true, false, true),
|
||||
@ -79,17 +71,12 @@ pub fn Content() -> Element {
|
||||
});
|
||||
|
||||
rsx! {
|
||||
VoiceChannelLayout {
|
||||
channel_name: "Project Alpha / Voice Lounge".to_string(),
|
||||
channel_topic: "Team sync & architecture deep dive".to_string(),
|
||||
participants,
|
||||
peer_id,
|
||||
remote_id,
|
||||
connected,
|
||||
websocket,
|
||||
responder_connection,
|
||||
initiator_connection,
|
||||
local_media,
|
||||
SignalingProvider {
|
||||
VoiceChannelLayout {
|
||||
channel_name: "Project Alpha / Voice Lounge".to_string(),
|
||||
channel_topic: "Team sync & architecture deep dive".to_string(),
|
||||
participants,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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