Added MediaManager for WebRTC functions.

This commit is contained in:
ghost 2025-08-21 16:12:23 +02:00
parent c6a119c494
commit 18a26c6bf1
7 changed files with 342 additions and 53 deletions

8
Cargo.lock generated
View File

@ -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]]

View File

@ -1,26 +1,46 @@
[package]
name = "niom-webrtc2"
name = "niom-webrtc"
version = "0.1.0"
authors = ["ghost <ma-koenig@gmx.net>"]
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

View File

@ -3,7 +3,7 @@
[web.app]
# HTML title tag content
title = "niom-webrtc2"
title = "niom-webrtc"
# include `assets` in web platform
[web.resource]

View File

@ -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;
}

View File

@ -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<bool>,
audio_enabled: Signal<bool>,
mut media_manager: Signal<MediaManager>,
on_start_call: EventHandler<MouseEvent>,
on_end_call: EventHandler<MouseEvent>,
on_toggle_audio: EventHandler<MouseEvent>
@ -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<bool>,
audio_enabled: Signal<bool>,
local_peer_id: Signal<String>,
remote_peer_id: Signal<String>
remote_peer_id: Signal<String>,
media_manager: Signal<MediaManager>
) -> Element {
rsx! {
div {
class: "status-display",
h2 { "Status" }
div {
class: "status-item",
span {
class: "status-label",
"Mikrofon Berechtigung:"
}
span {
class: match &media_manager.read().state {
MediaState::Granted(_) => "status-value connected",
MediaState::Denied(_) => "status-value disconnected",
MediaState::Requesting => "status-value requesting",
_ => "status-value unknown"
},
match &media_manager.read().state {
MediaState::Granted(_) => "Erteilt",
MediaState::Denied(_) => "Verweigert",
MediaState::Requesting => "Anfrage läuft",
_ => "Nicht verfügbar"
}
}
}
div {
class: "status-item",
span {
class: "status-label",
"Mikrofon:"
}
span {
class:
if audio_enabled() {
"status-value connected"
}
else {
"status-value disconnected"
},
if audio_enabled() {
"Entmuted"
}
else {
"Stumm"
}
}
}
div {
class: "status-item",
span {
@ -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"
}
}
}
}
}

130
src/utils/media_manager.rs Normal file
View File

@ -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<MediaStream, String> {
// 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(_))
}
}

3
src/utils/mod.rs Normal file
View File

@ -0,0 +1,3 @@
mod media_manager;
pub use media_manager::*;