From 18a26c6bf1bd5affc9f9a864521f5b1a150fb74c Mon Sep 17 00:00:00 2001 From: ghost Date: Thu, 21 Aug 2025 16:12:23 +0200 Subject: [PATCH] Added MediaManager for WebRTC functions. --- Cargo.lock | 8 +- Cargo.toml | 36 +++++++-- Dioxus.toml | 2 +- assets/main.css | 61 +++++++++++++++ src/main.rs | 155 +++++++++++++++++++++++++++---------- src/utils/media_manager.rs | 130 +++++++++++++++++++++++++++++++ src/utils/mod.rs | 3 + 7 files changed, 342 insertions(+), 53 deletions(-) create mode 100644 src/utils/media_manager.rs create mode 100644 src/utils/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 4be41f6..34780f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2663,14 +2663,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] -name = "niom-webrtc2" +name = "niom-webrtc" version = "0.1.0" dependencies = [ "console_error_panic_hook", "dioxus", "dioxus-logger", + "js-sys", "log", + "serde", + "serde_json", "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 56e7386..9960904 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,26 +1,46 @@ [package] -name = "niom-webrtc2" +name = "niom-webrtc" version = "0.1.0" authors = ["ghost "] edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -dioxus = { version = "0.6.0", features = [] } -console_error_panic_hook = "0.1.7" -tracing = "0.1" +# Dioxus Framework +dioxus = { version = "0.6.0", features = ["web"] } dioxus-logger = "0.6.2" +console_error_panic_hook = "0.1.7" + +# WebAssembly and Browser APIs +wasm-bindgen = "0.2.84" +wasm-bindgen-futures = "0.4.28" +js-sys = "0.3.61" + +# web-sys with features for media devices +web-sys = { version = "0.3.77", features = [ + "Navigator", + "MediaDevices", + "MediaStream", + "MediaStreamConstraints", + "MediaStreamTrack", + "MediaTrackSettings", + "MediaTrackConstraints", + "AudioContext" +]} + +# Logging and Tracing +tracing = "0.1" log = "0.4.27" +# Serialization +serde = { version = "1.0.142", features = ["derive"] } +serde_json = "1.0.100" + [features] default = ["web"] web = ["dioxus/web"] desktop = ["dioxus/desktop"] mobile = ["dioxus/mobile"] -[profile] - [profile.wasm-dev] inherits = "dev" opt-level = 1 diff --git a/Dioxus.toml b/Dioxus.toml index cb74a1d..0e91bb0 100644 --- a/Dioxus.toml +++ b/Dioxus.toml @@ -3,7 +3,7 @@ [web.app] # HTML title tag content -title = "niom-webrtc2" +title = "niom-webrtc" # include `assets` in web platform [web.resource] diff --git a/assets/main.css b/assets/main.css index 7643919..ce7a0fa 100644 --- a/assets/main.css +++ b/assets/main.css @@ -207,6 +207,10 @@ button:disabled:hover { color: #10b981; } +.status-value.requesting { + color: #f59e0b; +} + /* Responsive Design */ @media (min-width: 768px) { .main-content { @@ -259,3 +263,60 @@ button:disabled:hover { border-radius: 4px; color: #374151; } + +/* Mic Test */ +.mic-test-section { + margin-bottom: 20px; + padding: 16px; + border: 2px solid #e5e7eb; + border-radius: 8px; + background-color: #f9fafb; +} + +.mic-test-section h3 { + margin-bottom: 12px; + color: #374151; + font-size: 16px; +} + +.mic-test-btn { + background-color: #059669; + color: white; + margin-right: 8px; +} + +.mic-test-btn:hover:not(:disabled) { + background-color: #047857; +} + +.mic-test-btn:disabled { + background-color: #6b7280; + cursor: not-allowed; +} + +.mic-stop-btn { + background-color: #ef4444; + color: white; +} + +.mic-stop-btn:hover { + background-color: #dc2626; +} + +/* Warning Message */ +.warning-message { + padding: 12px; + background-color: #fef3c7; + border: 1px solid #f59e0b; + border-radius: 6px; + color: #92400e; + font-weight: 500; + margin-top: 12px; +} + +/* Call Button mit zusätzlicher Disabled-State */ +.call-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + background-color: #9ca3af !important; +} diff --git a/src/main.rs b/src/main.rs index fa8996c..57b5987 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,15 @@ #![allow(non_snake_case)] -use dioxus::prelude::*; +mod utils; + +use dioxus::{html::{g::media, h3}, prelude::*}; +use utils::{MediaManager, MediaState}; const FAVICON: Asset = asset!("/assets/favicon.ico"); const MAIN_CSS: Asset = asset!("/assets/main.css"); const HEADER_SVG: Asset = asset!("/assets/header.svg"); fn main() { - // Logger initialisieren für besseres Debugging - // dioxus_logger::init(log::Level::Info).expect("failed to init logger"); - // console_error_panic_hook::set_once(); - dioxus::launch(App); } @@ -20,20 +19,32 @@ fn App() -> Element { document::Link { rel: "icon", href: FAVICON } document::Link { rel: "stylesheet", href: MAIN_CSS } Content {} - } } #[component] pub fn Content() ->Element { // State for connection status and audio - let mut connected = use_signal(|| false); // Status: Verbindung aufgebaut? - let mut audio_enabled = use_signal(|| true); // Status: Mikro aktiviert? + let mut connected = use_signal(|| false); + let mut audio_enabled = use_signal(|| true); // State for Peer IDs let mut local_peer_id = use_signal(|| generate_peer_id()); let mut remote_peer_id = use_signal(|| String::new()); + let mut media_manager = use_signal(|| MediaManager::new()); + + // On mount: Request microphone access if not already granted + use_effect(move || { + to_owned![media_manager]; + spawn(async move { + match media_manager.write().request_microphone_access().await { + Ok(_) => log::info!("Microphone access granted"), + Err(e) => log::error!("Failed to request microphone access: {}", e) + } + }); + }); + rsx! { div { class: "app-container", @@ -49,9 +60,9 @@ pub fn Content() ->Element { // Connection Panel ConnectionPanel { - connected, - local_peer_id, - remote_peer_id, + connected: connected.clone(), + local_peer_id: local_peer_id.clone(), + remote_peer_id: remote_peer_id.clone(), on_connect: move |_| { log::info!("Verbindung wird hergestellt..."); connected.set(true); @@ -60,27 +71,30 @@ pub fn Content() ->Element { // Call Controls CallControls { - connected, - audio_enabled, + connected: connected.clone(), + audio_enabled: audio_enabled.clone(), + media_manager: media_manager.clone(), on_start_call: move |_| { log::info!("Anruf wird gestartet mit Remote Peer: {}", remote_peer_id()); }, on_end_call: move |_| { log::info!("Anruf wird beendet"); connected.set(false); + media_manager.write().stop_stream(); }, on_toggle_audio: move |_| { audio_enabled.set(!audio_enabled()); log::info!("Audio Status: {}", if audio_enabled() { "Aktiviert" } else { "Deaktiviert" }); - } + } } // Status Display StatusDisplay { - connected, - audio_enabled, - local_peer_id, - remote_peer_id + connected: connected.clone(), + audio_enabled: audio_enabled.clone(), + local_peer_id: local_peer_id.clone(), + remote_peer_id: remote_peer_id.clone(), + media_manager: media_manager.clone() } } } @@ -102,12 +116,12 @@ fn ConnectionPanel( div { class: "input-group", - label { "for": "local-peer-id", "Ihre Peer ID:" } + label { r#for: "local-peer-id", "Ihre Peer ID:" } input { id: "local-peer-id", class: "readonly-input", r#type: "text", - value: "{local_peer_id}", + value: "{local_peer_id()}", readonly: true } button { @@ -122,7 +136,7 @@ fn ConnectionPanel( div { class: "input-group", - label { "for": "remote-peer-id", "Remote Peer-ID:" } + label { r#for: "remote-peer-id", "Remote Peer-ID:" } input { id: "remote-peer-id", r#type: "text", @@ -155,6 +169,7 @@ fn ConnectionPanel( fn CallControls( connected: Signal, audio_enabled: Signal, + mut media_manager: Signal, on_start_call: EventHandler, on_end_call: EventHandler, on_toggle_audio: EventHandler @@ -163,19 +178,24 @@ fn CallControls( div { class: "call-controls", h2 { "Anruf-Steuerung" } - + div { class: "control-buttons", + + // Start Call button { class: "call-btn primary", onclick: on_start_call, disabled: !connected(), "📞 Anruf starten" } + + // Toggle Audio button { class: if audio_enabled() { "mute-btn" - } else { + } + else { "mute-btn-muted" }, onclick: on_toggle_audio, @@ -187,6 +207,8 @@ fn CallControls( "🔇 Stumm" } } + + // End Call button { class: "end-btn danger", onclick: on_end_call, @@ -204,13 +226,59 @@ fn StatusDisplay( connected: Signal, audio_enabled: Signal, local_peer_id: Signal, - remote_peer_id: Signal + remote_peer_id: Signal, + media_manager: Signal ) -> Element { rsx! { div { class: "status-display", h2 { "Status" } + div { + class: "status-item", + span { + class: "status-label", + "Mikrofon Berechtigung:" + } + span { + class: match &media_manager.read().state { + MediaState::Granted(_) => "status-value connected", + MediaState::Denied(_) => "status-value disconnected", + MediaState::Requesting => "status-value requesting", + _ => "status-value unknown" + }, + match &media_manager.read().state { + MediaState::Granted(_) => "Erteilt", + MediaState::Denied(_) => "Verweigert", + MediaState::Requesting => "Anfrage läuft", + _ => "Nicht verfügbar" + } + } + } + + div { + class: "status-item", + span { + class: "status-label", + "Mikrofon:" + } + span { + class: + if audio_enabled() { + "status-value connected" + } + else { + "status-value disconnected" + }, + if audio_enabled() { + "Entmuted" + } + else { + "Stumm" + } + } + } + div { class: "status-item", span { @@ -246,30 +314,24 @@ fn StatusDisplay( "Nicht verbunden" } } - - div { - class: "status-item", + + div { + class:"status-item", span { class: "status-label", - "Audio Status:" + "Mikrofon Status:" } - span { - class: - if audio_enabled() { - "status-value" - } - else { - "status-value disconnected" - }, - if audio_enabled() { - "Aktiviert" - } - else { - "Stumm" - } + span { + class: match &media_manager.read().state { + MediaState::Granted(_) => "status-value connected", + MediaState::Denied(_) => "status-value disconnected", + MediaState::Requesting => "status-value requesting", + _ => "status-value" + }, + "{media_manager.read().get_status_text()}" } } - + div { class: "status-item", span { @@ -295,6 +357,13 @@ fn StatusDisplay( } } } + + if !MediaManager::is_webrtc_supported() { + div { + class: "warning-message", + "⚠️ WebRTC wird von diesem Browser nicht unterstützt" + } + } } } } diff --git a/src/utils/media_manager.rs b/src/utils/media_manager.rs new file mode 100644 index 0000000..5e16fe9 --- /dev/null +++ b/src/utils/media_manager.rs @@ -0,0 +1,130 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use web_sys::{MediaDevices, MediaStream, MediaStreamConstraints, Navigator, Window}; + +// Enum für verschiedene Media-Zustände +#[derive(Debug, Clone, PartialEq)] +pub enum MediaState { + Uninitialized, + Requesting, + Granted(MediaStream), + Denied(String), + NotSupported, +} + +// Media Manager für WebRTC-Funktionalität +pub struct MediaManager { + pub state: MediaState, +} + +impl MediaManager { + // Creates a new MediaManager instance + pub fn new() -> Self { + Self { + state: MediaState::Uninitialized, + } + } + + // Checks if WebRTC is supported + pub fn is_webrtc_supported() -> bool { + let window: Window = match web_sys::window() { + Some(w) => w, + None => return false, + }; + + let navigator: Navigator = window.navigator(); + navigator.media_devices().is_ok() + } + + pub async fn request_microphone_access(&mut self) -> Result { + // Check if WebRTC is supported + if !Self::is_webrtc_supported() { + self.state = MediaState::NotSupported; + return Err("WebRTC wird von diesem Browser nicht unterstützt.".to_string()); + } + + self.state = MediaState::Requesting; + + // Get browser window and navigator + let window = web_sys::window().ok_or("Kein Browserfenster gefunden")?; + let navigator = window.navigator(); + let media_devices = navigator.media_devices().map_err(|_| "MediaDevices API nicht verfügbar")?; + + // Define media constraints: only audio, no video + let mut constraints = MediaStreamConstraints::new(); + constraints.audio(&JsValue::from(true)); + constraints.video(&JsValue::from(false)); + + // Request access to the microphone + let promise = media_devices + .get_user_media_with_constraints(&constraints) + .map_err(|e| format!("getUserMedia fehlgeschlagen: {:?}", e))?; + + // Convert JavaScript Promise to Rust Future + let future = JsFuture::from(promise); + + match future.await { + Ok(stream) => { + // Convert JsValue to MediaStream + let media_stream: MediaStream = stream.dyn_into().map_err(|_| "Fehler beim Konvertieren zu MediaStream")?; + self.state = MediaState::Granted(media_stream.clone()); + + log::info!("Mikrofon-Zugriff erfolgreich erhalten!"); + + Ok(media_stream) + } + Err(e) => { + let error_message = format!("Mikrofon-Zugriff verweigert: {:?}", e); + + self.state = MediaState::Denied(error_message.clone()); + log::error!("{}", error_message); + Err(error_message) + } + } + } + + // Logs information about a media stream + fn log_stream_info(&self, stream: &MediaStream) { + let tracks = stream.get_audio_tracks(); + log::info!("Audio-Tracks erhalten: {}", tracks.length()); + + for i in 0..tracks.length() { + let track = tracks.get(i); + let track: web_sys::MediaStreamTrack = track.dyn_into().unwrap(); + log::info!("Track {}: {} ({})", i, track.label(), track.kind()); + } + } + + // Stops the media stream and all its tracks + pub fn stop_stream(&mut self) { + if let MediaState::Granted(ref stream) = self.state { + let tracks = stream.get_tracks(); + + for i in 0..tracks.length() { + let track = tracks.get(i); + let track: web_sys::MediaStreamTrack = track.dyn_into().unwrap(); + track.stop(); + log::info!("Track gestoppt: {}", track.label()); + } + } + + self.state = MediaState::Uninitialized; + log::info!("MediaStream gestoppt."); + } + + // Returns a user-friendly status text based on the current media state + pub fn get_status_text(&self) -> &str { + match self.state { + MediaState::Uninitialized => "Nicht initialisiert", + MediaState::Requesting => "Berechtigung wird angefragt...", + MediaState::Granted(_) => "Zugriff gewährt", + MediaState::Denied(_) => "Zugriff verweigert", + MediaState::NotSupported => "WebRTC wird nicht unterstützt", + } + } + + // Checks if the microphone is currently active + pub fn is_microphone_active(&self) -> bool { + matches!(self.state, MediaState::Granted(_)) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..896fbe2 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,3 @@ +mod media_manager; + +pub use media_manager::*;