Added MediaManager for WebRTC functions.
This commit is contained in:
parent
c6a119c494
commit
18a26c6bf1
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -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]]
|
||||
|
||||
36
Cargo.toml
36
Cargo.toml
@ -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
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "niom-webrtc2"
|
||||
title = "niom-webrtc"
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
155
src/main.rs
155
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<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
130
src/utils/media_manager.rs
Normal 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
3
src/utils/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod media_manager;
|
||||
|
||||
pub use media_manager::*;
|
||||
Loading…
x
Reference in New Issue
Block a user