webrtc: add runtime config README example; provider/consumer docs; minor fixes
This commit is contained in:
parent
9fa1c22c5e
commit
40a909c991
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2686,6 +2686,7 @@ dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
|
||||
@ -53,6 +53,9 @@ serde_json = "1.0.100"
|
||||
futures = "0.3.31"
|
||||
gloo-net = "0.6"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.6"
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
web = ["dioxus/web"]
|
||||
|
||||
90
README.md
90
README.md
@ -23,3 +23,93 @@ To run for a different platform, use the `--platform platform` flag. E.g.
|
||||
dx serve --platform desktop
|
||||
```
|
||||
|
||||
## Configuration (appsettings.json and WASM fast-path)
|
||||
|
||||
This project supports an `appsettings.json`-style configuration that is loaded at runtime.
|
||||
|
||||
- Native (desktop/server): the app tries to read `appsettings.json` from the working directory.
|
||||
- WASM (web): the loader will first try a small HTML-injection fast-path (see below). If that
|
||||
is not present it will fetch `appsettings.json` from the hosting origin.
|
||||
|
||||
Example `appsettings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"stun_server": "stun:stun.l.google.com:19302"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HTML injection fast-path (for embedding a small config directly in `index.html`):
|
||||
|
||||
```html
|
||||
<script id="app-config" type="application/json">{
|
||||
"server": { "stun_server": "stun:stun.l.google.com:19302" }
|
||||
}</script>
|
||||
```
|
||||
|
||||
This is useful for static hosting scenarios where you want to provide a different runtime
|
||||
configuration without rebuilding the WASM artifact.
|
||||
|
||||
## Consuming the Config in the UI (Dioxus)
|
||||
|
||||
The Dioxus app provides a `Signal<Config>` via the hooks context. Child components can consume
|
||||
it with:
|
||||
|
||||
```rust
|
||||
let cfg_signal = use_context::<Signal<niom_webrtc::config::Config>>();
|
||||
let cfg = cfg_signal.get(); // or cfg_signal.read()/() depending on the signal API
|
||||
// use cfg.server.stun_server
|
||||
```
|
||||
|
||||
The app uses an async loader (via `use_resource`) and overwrites the initial default config when
|
||||
the runtime-loaded config becomes available. This gives instant sensible defaults while still
|
||||
allowing runtime overrides.
|
||||
|
||||
### Example: Provider + Consumer (Dioxus)
|
||||
|
||||
Below is a minimal example showing how the app creates a `Signal<Config>` provider and how a
|
||||
child component consumes it. This pattern gives children an immediately-available default
|
||||
config while an async fetch can replace it at runtime.
|
||||
|
||||
```rust
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_hooks::use_resource;
|
||||
use niom_webrtc::config;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn ConfigProvider(cx: Scope) -> Element {
|
||||
// synchronous immediate default
|
||||
let default_cfg = config::load_config_sync_or_default();
|
||||
|
||||
// a reactive signal holding the current config
|
||||
let cfg_signal = use_state(cx, || default_cfg.clone());
|
||||
|
||||
// async resource that loads runtime config (WASM fetch or native file)
|
||||
let resource = use_resource(cx, (), |_| async move { config::load_config_or_default().await });
|
||||
|
||||
// when resource completes, update the signal so children react
|
||||
if let Some(cfg) = resource.value() {
|
||||
cfg_signal.set(cfg.clone());
|
||||
}
|
||||
|
||||
// provide the signal to descendants
|
||||
use_context_provider(cx, || cfg_signal.clone());
|
||||
|
||||
cx.render(rsx! { children(&cx) })
|
||||
}
|
||||
|
||||
fn SomeChild(cx: Scope) -> Element {
|
||||
let cfg_signal = use_context::<UseStateHandle<config::Config>>(cx);
|
||||
let cfg = cfg_signal.get();
|
||||
|
||||
cx.render(rsx!(div { "STUN: {cfg.server.stun_server}" }))
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `load_config_sync_or_default()` returns a sensible default immediately (plus HTML fast-path on WASM).
|
||||
- `load_config_or_default()` is async and will perform a network fetch on WASM if the HTML fast-path wasn't present.
|
||||
|
||||
|
||||
|
||||
@ -26,13 +26,74 @@ impl Config {
|
||||
// WASM runtime loader: fetch `appsettings.json` from the hosting origin
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn load_config_from_server() -> Result<Config, Box<dyn std::error::Error>> {
|
||||
// lazy import gloo-net to avoid non-wasm compile errors
|
||||
// First try HTML-injection fast-path: look for a <script id="app-config"> element
|
||||
if let Ok(Some(cfg)) = load_config_from_html().await {
|
||||
return Ok(cfg);
|
||||
}
|
||||
|
||||
// Fallback to fetching appsettings.json from the hosting origin
|
||||
let resp = gloo_net::http::Request::get("appsettings.json").send().await?;
|
||||
let text = resp.text().await?;
|
||||
let cfg: Config = serde_json::from_str(&text)?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
// Try to read a JSON config injected into index.html inside a script tag
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn load_config_from_html() -> Result<Option<Config>, Box<dyn std::error::Error>> {
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::window;
|
||||
|
||||
let win = window().ok_or("no window")?;
|
||||
let doc = win.document().ok_or("no document")?;
|
||||
if let Some(elem) = doc.get_element_by_id("app-config") {
|
||||
if let Some(text) = elem.text_content() {
|
||||
let cfg: Config = serde_json::from_str(&text)?;
|
||||
return Ok(Some(cfg));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Synchronous HTML fast-path for WASM: read script#app-config synchronously if present.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn load_config_from_html_sync() -> Option<Config> {
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::window;
|
||||
|
||||
let win = window().ok()?;
|
||||
let doc = win.document()?;
|
||||
let elem = doc.get_element_by_id("app-config")?;
|
||||
if let Some(text) = elem.text_content() {
|
||||
if let Ok(cfg) = serde_json::from_str::<Config>(&text) {
|
||||
return Some(cfg);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Synchronous loader that prefers the HTML-injection fast-path on WASM, otherwise reads file on native.
|
||||
pub fn load_config_sync_or_default() -> Config {
|
||||
// WASM: try HTML-injection first
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
if let Some(cfg) = load_config_from_html_sync() {
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
|
||||
// Native: try reading appsettings.json from file
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
if let Ok(cfg) = Config::from_file("appsettings.json") {
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback default
|
||||
Config { server: ServerOptions { stun_server: crate::constants::DEFAULT_STUN_SERVER.to_string() } }
|
||||
}
|
||||
|
||||
// Native loader convenience wrapper (blocking-friendly)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn load_config_from_server() -> Result<Config, Box<dyn std::error::Error>> {
|
||||
|
||||
10
src/lib.rs
Normal file
10
src/lib.rs
Normal file
@ -0,0 +1,10 @@
|
||||
// Library root for niom-webrtc so integration tests and other crates can depend on the modules.
|
||||
pub mod components;
|
||||
pub mod models;
|
||||
pub mod utils;
|
||||
pub mod config;
|
||||
pub mod constants;
|
||||
|
||||
// Re-export commonly used items if needed in the future
|
||||
// pub use config::*;
|
||||
// pub use constants::*;
|
||||
65
src/main.rs
65
src/main.rs
@ -1,16 +1,11 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
mod components;
|
||||
mod models;
|
||||
mod utils;
|
||||
mod config;
|
||||
mod constants;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use log::Level;
|
||||
use console_log::init_with_level;
|
||||
use components::{ConnectionPanel, CallControls, StatusDisplay};
|
||||
use niom_webrtc::components::{ConnectionPanel, CallControls, StatusDisplay};
|
||||
use web_sys::{RtcPeerConnection, WebSocket as BrowserWebSocket, MediaStream};
|
||||
// config functions used via fully-qualified paths below
|
||||
|
||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||
const MAIN_CSS: Asset = asset!("/assets/main.css");
|
||||
@ -22,31 +17,47 @@ fn main() {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> Element {
|
||||
// Component state for loaded config
|
||||
let cfg_state = use_state(cx, || None::<crate::config::Config>);
|
||||
|
||||
// Kick off async config load once
|
||||
use_future(cx, (), |_| {
|
||||
let cfg_state = cfg_state.clone();
|
||||
async move {
|
||||
let cfg = crate::config::load_config_or_default().await;
|
||||
cfg_state.set(Some(cfg));
|
||||
}
|
||||
});
|
||||
|
||||
match cfg_state.get().as_ref() {
|
||||
None => rsx!( div { "Lade Konfiguration…" } ),
|
||||
Some(_cfg) => rsx! {
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||
Content {}
|
||||
}
|
||||
fn App() -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||
ConfigProvider {}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ConfigProvider() -> Element {
|
||||
// Start with a synchronous default so the UI has a sensible config immediately.
|
||||
let default_cfg = niom_webrtc::config::load_config_sync_or_default();
|
||||
|
||||
// A signal holding the active config; children will consume a Signal<Config> via use_context.
|
||||
let mut cfg_signal = use_signal(|| default_cfg.clone());
|
||||
|
||||
// Spawn an async resource to load the config at runtime (WASM fetch / native file). When it
|
||||
// finishes, overwrite the signal so the rest of the app sees the updated config.
|
||||
let resource = use_resource(|| async move {
|
||||
// load_config_or_default is async and returns a Config
|
||||
niom_webrtc::config::load_config_or_default().await
|
||||
});
|
||||
|
||||
// When the resource value becomes available, update the cfg_signal. This effect will run
|
||||
// reactively whenever the resource's value changes.
|
||||
// If the async resource has produced a value, apply it to the signal immediately so
|
||||
// downstream consumers see the updated configuration.
|
||||
if let Some(cfg) = resource.value().read_unchecked().as_ref() {
|
||||
cfg_signal.set(cfg.clone());
|
||||
}
|
||||
|
||||
// Provide the Signal<Config> to downstream components.
|
||||
use_context_provider(|| cfg_signal.clone());
|
||||
|
||||
rsx! { Content {} }
|
||||
}
|
||||
|
||||
#[component]
|
||||
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);
|
||||
|
||||
@ -2,11 +2,9 @@ use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{
|
||||
MediaStream, MediaStreamConstraints, Navigator, Window,
|
||||
MediaStream, MediaStreamConstraints,
|
||||
RtcPeerConnection, RtcConfiguration, RtcIceServer,
|
||||
// SDP-Types
|
||||
RtcSignalingState,
|
||||
RtcSessionDescription, RtcSessionDescriptionInit, RtcSdpType,
|
||||
RtcSessionDescriptionInit, RtcSdpType,
|
||||
};
|
||||
use js_sys::Reflect;
|
||||
use crate::models::MediaState;
|
||||
@ -100,17 +98,12 @@ impl MediaManager {
|
||||
log::info!("📨 Handling received answer...");
|
||||
|
||||
// **DEBUG:** State vor Answer-Verarbeitung
|
||||
// Use the signaling_state() result for debug but avoid importing the enum type locally.
|
||||
let state = pc.signaling_state();
|
||||
log::info!("🔍 PeerConnection state before answer: {:?}", state);
|
||||
|
||||
// **NUR** verarbeiten wenn im korrekten State
|
||||
match state {
|
||||
web_sys::RtcSignalingState::HaveLocalOffer => {
|
||||
log::info!("✅ Korrekter State - verarbeite Answer");
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("❌ Falscher State für Answer: {:?}", state));
|
||||
}
|
||||
// Only proceed if in HaveLocalOffer
|
||||
if state != web_sys::RtcSignalingState::HaveLocalOffer {
|
||||
return Err(format!("❌ Falscher State für Answer: {:?}", state));
|
||||
}
|
||||
|
||||
let init = RtcSessionDescriptionInit::new(RtcSdpType::Answer);
|
||||
|
||||
19
tests/config_sync_tests.rs
Normal file
19
tests/config_sync_tests.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use niom_webrtc::config::load_config_sync_or_default;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn sync_loader_returns_default_when_no_file() {
|
||||
// Ensure there's no appsettings.json in CWD for this test
|
||||
let _ = fs::remove_file("appsettings.json");
|
||||
let cfg = load_config_sync_or_default();
|
||||
assert_eq!(cfg.server.stun_server, niom_webrtc::constants::DEFAULT_STUN_SERVER.to_string());
|
||||
}
|
||||
|
||||
// This test ensures the function compiles and returns a Config; on native it will use the file-path,
|
||||
// on wasm it would read the HTML-injection. We call it to assert the API surface.
|
||||
#[test]
|
||||
fn sync_loader_api_callable() {
|
||||
let cfg = load_config_sync_or_default();
|
||||
// At minimum we have a non-empty stun_server
|
||||
assert!(!cfg.server.stun_server.is_empty());
|
||||
}
|
||||
17
tests/config_tests.rs
Normal file
17
tests/config_tests.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use niom_webrtc::constants::DEFAULT_STUN_SERVER;
|
||||
use niom_webrtc::config::Config;
|
||||
|
||||
#[test]
|
||||
fn default_stun_server_present() {
|
||||
assert!(DEFAULT_STUN_SERVER.contains("stun:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_from_file_roundtrip() {
|
||||
// Create a temporary JSON in /tmp and read it via Config::from_file
|
||||
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
|
||||
let cfg_json = r#"{ "server": { "stun_server": "stun:example.org:3478" } }"#;
|
||||
std::fs::write(tmp.path(), cfg_json).expect("write");
|
||||
let cfg = Config::from_file(tmp.path()).expect("load");
|
||||
assert_eq!(cfg.server.stun_server, "stun:example.org:3478");
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user