Completed ICE. Audio is transmitted to other client.
This commit is contained in:
parent
cb0d2765d3
commit
676a7fae24
@ -27,6 +27,8 @@ web-sys = { version = "0.3.77", features = [
|
|||||||
"MediaTrackSettings",
|
"MediaTrackSettings",
|
||||||
"MediaTrackConstraints",
|
"MediaTrackConstraints",
|
||||||
"AudioContext",
|
"AudioContext",
|
||||||
|
"HtmlAudioElement",
|
||||||
|
"HtmlMediaElement",
|
||||||
"RtcPeerConnection",
|
"RtcPeerConnection",
|
||||||
"RtcConfiguration",
|
"RtcConfiguration",
|
||||||
"RtcIceServer",
|
"RtcIceServer",
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use web_sys::{WebSocket as BrowserWebSocket, RtcPeerConnection};
|
use web_sys::{WebSocket as BrowserWebSocket, RtcPeerConnection, MediaStream};
|
||||||
use crate::models::SignalingMessage;
|
use crate::models::SignalingMessage;
|
||||||
use crate::utils::MediaManager;
|
use crate::utils::MediaManager;
|
||||||
use futures::StreamExt;
|
use wasm_bindgen::prelude::Closure;
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CallControls(
|
pub fn CallControls(
|
||||||
@ -11,47 +13,12 @@ pub fn CallControls(
|
|||||||
connected: Signal<bool>,
|
connected: Signal<bool>,
|
||||||
websocket: Signal<Option<BrowserWebSocket>>,
|
websocket: Signal<Option<BrowserWebSocket>>,
|
||||||
peer_connection: Signal<Option<RtcPeerConnection>>, // **INITIATOR CONNECTION**
|
peer_connection: Signal<Option<RtcPeerConnection>>, // **INITIATOR CONNECTION**
|
||||||
|
local_media: Signal<Option<MediaStream>>,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let mut mic_granted = use_signal(|| false);
|
let mic_granted = use_signal(|| false);
|
||||||
let mut audio_muted = use_signal(|| false);
|
let mut audio_muted = use_signal(|| false);
|
||||||
let mut in_call = use_signal(|| false);
|
let mut in_call = use_signal(|| false);
|
||||||
|
|
||||||
// **COROUTINE** für Answer-Handling (Initiator empfängt Answers)
|
|
||||||
let answer_handler = 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 == "answer" {
|
|
||||||
log::info!("📞 WebRTC-Answer von {} als Initiator verarbeiten", from);
|
|
||||||
|
|
||||||
if let Some(pc) = peer_connection.read().as_ref() {
|
|
||||||
// **DEBUG:** State checken vor Answer
|
|
||||||
let signaling_state = pc.signaling_state();
|
|
||||||
log::info!("🔍 Initiator PeerConnection State vor Answer: {:?}", signaling_state);
|
|
||||||
|
|
||||||
// **NUR** Answer verarbeiten wenn im korrekten State (have-local-offer)
|
|
||||||
match signaling_state {
|
|
||||||
web_sys::RtcSignalingState::HaveLocalOffer => {
|
|
||||||
log::info!("✅ Korrekter State für Answer - Initiator verarbeitet");
|
|
||||||
match MediaManager::handle_answer(&pc, &data).await {
|
|
||||||
Ok(_) => {
|
|
||||||
log::info!("🎉 WebRTC-Handshake als Initiator abgeschlossen!");
|
|
||||||
in_call.set(true);
|
|
||||||
}
|
|
||||||
Err(e) => log::error!("❌ Initiator Answer-Verarbeitung: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log::warn!("⚠️ Answer ignoriert - Initiator PC im falschen State: {:?}", signaling_state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::error!("❌ Keine Initiator PeerConnection für Answer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "call-controls",
|
div { class: "call-controls",
|
||||||
h2 { "Anruf-Steuerung" }
|
h2 { "Anruf-Steuerung" }
|
||||||
@ -62,8 +29,29 @@ pub fn CallControls(
|
|||||||
class: "mic-permission-btn primary",
|
class: "mic-permission-btn primary",
|
||||||
disabled: *mic_granted.read(),
|
disabled: *mic_granted.read(),
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
mic_granted.set(true);
|
log::info!("🎤 Fordere Mikrofon-Berechtigung an...");
|
||||||
log::info!("🎤 Mikrofon-Berechtigung simuliert");
|
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!("✅ Mikrofonzugang erteilt");
|
||||||
|
mm_state.set(true);
|
||||||
|
// Speichere den Stream global, damit andere Komponenten ihn nutzen können
|
||||||
|
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!("Fehler beim Hinzufügen der lokalen Tracks: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("❌ Mikrofonzugriff fehlgeschlagen: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
if *mic_granted.read() {
|
if *mic_granted.read() {
|
||||||
"✅ Berechtigung erteilt"
|
"✅ Berechtigung erteilt"
|
||||||
@ -85,13 +73,64 @@ pub fn CallControls(
|
|||||||
let ws_signal = websocket.clone();
|
let ws_signal = websocket.clone();
|
||||||
let from_id = peer_id.read().clone();
|
let from_id = peer_id.read().clone();
|
||||||
let to_id = remote_id.read().clone();
|
let to_id = remote_id.read().clone();
|
||||||
let handler_tx = answer_handler.clone();
|
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
// **INITIATOR:** PeerConnection erstellen
|
// **INITIATOR:** PeerConnection erstellen
|
||||||
let pc = if pc_signal.read().is_none() {
|
let pc = if pc_signal.read().is_none() {
|
||||||
match MediaManager::create_peer_connection() {
|
match MediaManager::create_peer_connection() {
|
||||||
Ok(new_pc) => {
|
Ok(new_pc) => {
|
||||||
|
// Attach onicecandidate handler to send candidates via websocket
|
||||||
|
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();
|
||||||
|
|
||||||
|
// ontrack -> play remote audio
|
||||||
|
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();
|
||||||
|
|
||||||
pc_signal.set(Some(new_pc.clone()));
|
pc_signal.set(Some(new_pc.clone()));
|
||||||
log::info!("✅ Initiator PeerConnection erstellt");
|
log::info!("✅ Initiator PeerConnection erstellt");
|
||||||
new_pc
|
new_pc
|
||||||
@ -105,6 +144,18 @@ pub fn CallControls(
|
|||||||
pc_signal.read().as_ref().unwrap().clone()
|
pc_signal.read().as_ref().unwrap().clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Falls wir bereits Zugriff auf Mikrofon haben, versuchen wir die
|
||||||
|
// lokalen Tracks erneut hinzuzufügen (no-op, falls schon vorhanden).
|
||||||
|
// Hinweis: In dieser einfachen Struktur halten wir den MediaStream
|
||||||
|
// nicht global, daher ist dies ein best-effort.
|
||||||
|
|
||||||
|
// Falls ein lokaler MediaStream vorhanden ist, stelle sicher, dass seine Tracks angehängt sind
|
||||||
|
if let Some(local) = local_media.read().as_ref() {
|
||||||
|
if let Err(e) = crate::utils::MediaManager::add_stream_to_pc(&pc, local) {
|
||||||
|
log::warn!("Fehler beim Hinzufügen der lokalen Tracks vor Offer: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// **INITIATOR:** Offer erstellen und senden
|
// **INITIATOR:** Offer erstellen und senden
|
||||||
match MediaManager::create_offer(&pc).await {
|
match MediaManager::create_offer(&pc).await {
|
||||||
Ok(offer_sdp) => {
|
Ok(offer_sdp) => {
|
||||||
@ -121,10 +172,8 @@ pub fn CallControls(
|
|||||||
log::info!("📤 WebRTC-Offer als Initiator gesendet an {}", to_id);
|
log::info!("📤 WebRTC-Offer als Initiator gesendet an {}", to_id);
|
||||||
|
|
||||||
// **SETUP:** Answer-Handler für eingehende Answers
|
// **SETUP:** Answer-Handler für eingehende Answers
|
||||||
if let Some(socket_clone) = ws_signal.read().as_ref() {
|
// Note: Answer wird über connection_panel's onmessage empfangen
|
||||||
// Note: Answer wird über connection_panel's onmessage empfangen
|
// und an diese Coroutine weitergeleitet
|
||||||
// und an diese Coroutine weitergeleitet
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use web_sys::{WebSocket as BrowserWebSocket, RtcPeerConnection, BinaryType, MessageEvent};
|
use web_sys::{WebSocket as BrowserWebSocket, RtcPeerConnection, BinaryType, MessageEvent};
|
||||||
use wasm_bindgen::prelude::{Closure, JsValue};
|
use web_sys::MediaStream;
|
||||||
|
use wasm_bindgen::prelude::Closure;
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
use crate::models::SignalingMessage;
|
use crate::models::SignalingMessage;
|
||||||
use crate::utils::MediaManager;
|
use crate::utils::MediaManager;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
@ -13,8 +16,12 @@ pub fn ConnectionPanel(
|
|||||||
mut connected: Signal<bool>,
|
mut connected: Signal<bool>,
|
||||||
mut websocket: Signal<Option<BrowserWebSocket>>,
|
mut websocket: Signal<Option<BrowserWebSocket>>,
|
||||||
peer_connection: Signal<Option<RtcPeerConnection>>, // **RESPONDER CONNECTION**
|
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 {
|
) -> Element {
|
||||||
let mut ws_status = use_signal(|| "Nicht verbunden".to_string());
|
let mut ws_status = use_signal(|| "Nicht verbunden".to_string());
|
||||||
|
// Buffer for an incoming Answer SDP if the initiator PC isn't ready yet
|
||||||
|
let mut pending_answer = use_signal(|| None::<String>);
|
||||||
|
|
||||||
// **COROUTINE** für Offer-Handling (Responder empfängt Offers)
|
// **COROUTINE** für Offer-Handling (Responder empfängt Offers)
|
||||||
let offer_handler = use_coroutine(move |mut rx| async move {
|
let offer_handler = use_coroutine(move |mut rx| async move {
|
||||||
@ -32,8 +39,65 @@ pub fn ConnectionPanel(
|
|||||||
let pc = if peer_connection.read().is_none() {
|
let pc = if peer_connection.read().is_none() {
|
||||||
match MediaManager::create_peer_connection() {
|
match MediaManager::create_peer_connection() {
|
||||||
Ok(new_pc) => {
|
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()));
|
peer_connection.set(Some(new_pc.clone()));
|
||||||
log::info!("✅ Responder PeerConnection für {} erstellt", from);
|
let from_for_log = from_clone.clone();
|
||||||
|
log::info!("✅ Responder PeerConnection für {} erstellt", from_for_log);
|
||||||
new_pc
|
new_pc
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -80,6 +144,9 @@ pub fn ConnectionPanel(
|
|||||||
log::info!("🆔 Peer-ID generiert: {}", id);
|
log::info!("🆔 Peer-ID generiert: {}", id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Einfacher Status-String für das Mikrofon
|
||||||
|
let mic_status = if local_media.read().is_some() { "✅ Erteilt" } else { "✖️ Nicht erteilt" };
|
||||||
|
|
||||||
// WebSocket verbinden
|
// WebSocket verbinden
|
||||||
let connect_websocket = move |_| {
|
let connect_websocket = move |_| {
|
||||||
log::info!("🔌 Verbinde WebSocket...");
|
log::info!("🔌 Verbinde WebSocket...");
|
||||||
@ -120,13 +187,47 @@ pub fn ConnectionPanel(
|
|||||||
offer_tx.send(msg);
|
offer_tx.send(msg);
|
||||||
}
|
}
|
||||||
"answer" => {
|
"answer" => {
|
||||||
log::info!("🔀 Answer empfangen - müsste an Initiator weitergeleitet werden");
|
log::info!("🔀 Answer empfangen - leite an Initiator-PeerConnection weiter");
|
||||||
// **PROBLEM:** Hier müssen wir eine Referenz zum Call-Controls Answer-Handler haben
|
let data_clone = msg.data.clone();
|
||||||
// **LÖSUNG:** Globaler Message-Bus oder direkte Referenz
|
if let Some(pc) = initiator_connection.read().as_ref() {
|
||||||
|
// Versuche die Answer als Remote Description zu setzen
|
||||||
// **WORKAROUND:** Temporär loggen
|
let pc_clone = pc.clone();
|
||||||
log::info!("📞 WebRTC-Answer für Initiator empfangen (noch nicht weitergeleitet)");
|
spawn_local(async move {
|
||||||
// TODO: An call_controls answer_handler weiterleiten
|
match crate::utils::MediaManager::handle_answer(&pc_clone, &data_clone).await {
|
||||||
|
Ok(_) => log::info!("✅ Answer erfolgreich gesetzt auf Initiator-PC"),
|
||||||
|
Err(e) => log::error!("❌ Fehler beim Setzen der Answer auf Initiator-PC: {}", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Buffer the answer until an initiator PC exists
|
||||||
|
log::warn!("⚠️ Keine Initiator-PeerConnection vorhanden - buffer Answer");
|
||||||
|
pending_answer.set(Some(data_clone));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"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" => {
|
"text" => {
|
||||||
log::info!("💬 Textnachricht: {}", msg.data);
|
log::info!("💬 Textnachricht: {}", msg.data);
|
||||||
@ -161,6 +262,31 @@ pub fn ConnectionPanel(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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",
|
||||||
h2 { "Verbindung" }
|
h2 { "Verbindung" }
|
||||||
@ -173,6 +299,14 @@ pub fn ConnectionPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div { class: "status-item",
|
||||||
|
span { class: "status-label", "Mikrofon:" }
|
||||||
|
span {
|
||||||
|
class: if local_media.read().is_some() { "status-value connected" } else { "status-value disconnected" },
|
||||||
|
"{mic_status}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div { class: "input-group",
|
div { class: "input-group",
|
||||||
label { "Ihre Peer-ID:" }
|
label { "Ihre Peer-ID:" }
|
||||||
input {
|
input {
|
||||||
|
|||||||
11
src/main.rs
11
src/main.rs
@ -8,7 +8,7 @@ use dioxus::prelude::*;
|
|||||||
use log::Level;
|
use log::Level;
|
||||||
use console_log::init_with_level;
|
use console_log::init_with_level;
|
||||||
use components::{ConnectionPanel, CallControls, StatusDisplay};
|
use components::{ConnectionPanel, CallControls, StatusDisplay};
|
||||||
use web_sys::{RtcPeerConnection, WebSocket as BrowserWebSocket};
|
use web_sys::{RtcPeerConnection, WebSocket as BrowserWebSocket, MediaStream};
|
||||||
|
|
||||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||||
const MAIN_CSS: Asset = asset!("/assets/main.css");
|
const MAIN_CSS: Asset = asset!("/assets/main.css");
|
||||||
@ -36,6 +36,8 @@ pub fn Content() -> Element {
|
|||||||
let mut websocket = use_signal(|| None::<BrowserWebSocket>);
|
let mut websocket = use_signal(|| None::<BrowserWebSocket>);
|
||||||
let initiator_connection = use_signal(|| None::<RtcPeerConnection>);
|
let initiator_connection = use_signal(|| None::<RtcPeerConnection>);
|
||||||
let responder_connection = use_signal(|| None::<RtcPeerConnection>);
|
let responder_connection = use_signal(|| None::<RtcPeerConnection>);
|
||||||
|
// globaler Signal für den lokal freigegebenen MediaStream (Mikrofon)
|
||||||
|
let local_media = use_signal(|| None::<MediaStream>);
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "app-container",
|
div { class: "app-container",
|
||||||
@ -49,14 +51,17 @@ pub fn Content() -> Element {
|
|||||||
remote_id,
|
remote_id,
|
||||||
connected,
|
connected,
|
||||||
websocket,
|
websocket,
|
||||||
peer_connection: responder_connection
|
peer_connection: responder_connection,
|
||||||
|
initiator_connection: initiator_connection,
|
||||||
|
local_media: local_media
|
||||||
}
|
}
|
||||||
CallControls {
|
CallControls {
|
||||||
peer_id,
|
peer_id,
|
||||||
remote_id,
|
remote_id,
|
||||||
connected,
|
connected,
|
||||||
websocket,
|
websocket,
|
||||||
peer_connection: initiator_connection
|
peer_connection: initiator_connection,
|
||||||
|
local_media: local_media
|
||||||
}
|
}
|
||||||
StatusDisplay {
|
StatusDisplay {
|
||||||
connected,
|
connected,
|
||||||
|
|||||||
@ -167,10 +167,67 @@ impl MediaManager {
|
|||||||
.expect("Expected MediaStreamTrack");
|
.expect("Expected MediaStreamTrack");
|
||||||
// 3. Stoppen
|
// 3. Stoppen
|
||||||
track.stop();
|
track.stop();
|
||||||
log::info!("🛑 Track gestoppt: {}", track.label());
|
log::info!("\u{1F6D1} Track gestoppt: {}", track.label());
|
||||||
}
|
}
|
||||||
self.state = MediaState::Uninitialized;
|
self.state = MediaState::Uninitialized;
|
||||||
log::info!("🛑 MediaStream gestoppt.");
|
log::info!("\u{1F6D1} MediaStream gestoppt.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fügt alle Tracks eines `MediaStream` zur angegebenen `RtcPeerConnection` hinzu.
|
||||||
|
///
|
||||||
|
/// In WebRTC sollten Tracks einzeln mit `addTrack` hinzugefügt werden. Diese
|
||||||
|
/// Hilfsfunktion iteriert über alle Tracks des Streams und fügt sie hinzu.
|
||||||
|
pub fn add_stream_to_pc(pc: &RtcPeerConnection, stream: &MediaStream) -> Result<(), String> {
|
||||||
|
let tracks = stream.get_tracks();
|
||||||
|
for i in 0..tracks.length() {
|
||||||
|
let js_val = tracks.get(i);
|
||||||
|
let track: web_sys::MediaStreamTrack = js_val
|
||||||
|
.dyn_into()
|
||||||
|
.map_err(|_| "Failed to cast to MediaStreamTrack")?;
|
||||||
|
|
||||||
|
// add_track nimmt (track, stream) in JS. In web-sys gibt add_track einen
|
||||||
|
// RtcRtpSender zurück; wir ignorieren den Rückgabewert hier.
|
||||||
|
// `addTrack` ist in manchen web-sys-Versionen nicht direkt verfügbar.
|
||||||
|
// Wir rufen die JS-Funktion dynamisch auf: pc.addTrack(track, stream)
|
||||||
|
let add_fn = js_sys::Reflect::get(pc.as_ref(), &JsValue::from_str("addTrack")).map_err(|_| "Failed to get addTrack function".to_string())?;
|
||||||
|
let func: js_sys::Function = add_fn.dyn_into().map_err(|_| "addTrack is not a function".to_string())?;
|
||||||
|
let _ = func.call2(pc.as_ref(), &JsValue::from(track.clone()), &JsValue::from(stream.clone()));
|
||||||
|
log::info!("\u{2705} Track hinzugefügt: {}", track.label());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fügt einen empfangenen ICE-Candidate zur PeerConnection hinzu.
|
||||||
|
/// `candidate_json` ist ein JSON-String mit Feldern: candidate, sdpMid, sdpMLineIndex
|
||||||
|
pub fn add_ice_candidate(pc: &RtcPeerConnection, candidate_json: &str) -> Result<(), String> {
|
||||||
|
// Parse the JSON into a JsValue
|
||||||
|
// Ignore empty candidate payloads (some browsers send a final empty candidate)
|
||||||
|
if candidate_json.trim().is_empty() {
|
||||||
|
log::info!("🔇 Ignoring empty ICE candidate payload");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let js_val = js_sys::JSON::parse(candidate_json)
|
||||||
|
.map_err(|e| format!("Failed to parse candidate JSON: {:?}", e))?;
|
||||||
|
|
||||||
|
// Prepare a plain JS object: { candidate, sdpMid, sdpMLineIndex }
|
||||||
|
let obj = js_sys::Object::new();
|
||||||
|
if let Ok(candidate) = js_sys::Reflect::get(&js_val, &JsValue::from_str("candidate")) {
|
||||||
|
let _ = js_sys::Reflect::set(&obj, &JsValue::from_str("candidate"), &candidate);
|
||||||
|
}
|
||||||
|
if let Ok(sdp_mid) = js_sys::Reflect::get(&js_val, &JsValue::from_str("sdpMid")) {
|
||||||
|
let _ = js_sys::Reflect::set(&obj, &JsValue::from_str("sdpMid"), &sdp_mid);
|
||||||
|
}
|
||||||
|
if let Ok(idx) = js_sys::Reflect::get(&js_val, &JsValue::from_str("sdpMLineIndex")) {
|
||||||
|
let _ = js_sys::Reflect::set(&obj, &JsValue::from_str("sdpMLineIndex"), &idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call pc.addIceCandidate(obj) dynamically
|
||||||
|
let add_fn = js_sys::Reflect::get(pc.as_ref(), &JsValue::from_str("addIceCandidate"))
|
||||||
|
.map_err(|_| "Failed to get addIceCandidate function".to_string())?;
|
||||||
|
let func: js_sys::Function = add_fn.dyn_into().map_err(|_| "addIceCandidate is not a function".to_string())?;
|
||||||
|
let _ = func.call1(pc.as_ref(), &obj);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user