videocall_diagnostics/lib.rs
1/*
2 * Copyright 2025 Security Union LLC
3 *
4 * Licensed under either of
5 *
6 * * Apache License, Version 2.0
7 * (http://www.apache.org/licenses/LICENSE-2.0)
8 * * MIT license
9 * (http://opensource.org/licenses/MIT)
10 *
11 * at your option.
12 *
13 * Unless you explicitly state otherwise, any contribution intentionally
14 * submitted for inclusion in the work by you, as defined in the Apache-2.0
15 * license, shall be dual licensed as above, without any additional terms or
16 * conditions.
17 */
18
19//! Lightweight diagnostics event bus shared across the code-base.
20//! Works on both native and `wasm32` targets (no Tokio required).
21
22use once_cell::sync::Lazy;
23use serde::{Deserialize, Serialize};
24use std::ops::Deref;
25
26// === Diagnostic data structures ===
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
29pub struct DiagEvent {
30 /// Sub-system that produced this event (e.g. "neteq", "codec", "transport").
31 pub subsystem: &'static str,
32 /// Optional stream identifier (peer or media stream).
33 pub stream_id: Option<String>,
34 /// Unix time in milliseconds when the metric was captured.
35 pub ts_ms: u64,
36 /// Arbitrary key/value metrics.
37 pub metrics: Vec<Metric>,
38}
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct Metric {
42 pub name: &'static str,
43 pub value: MetricValue,
44}
45
46#[derive(Clone, Debug, Serialize, Deserialize)]
47#[serde(tag = "t", content = "v")]
48pub enum MetricValue {
49 I64(i64),
50 U64(u64),
51 F64(f64),
52 Text(String),
53}
54
55// === Simple global broadcast bus ===
56
57use async_broadcast::{broadcast, Receiver, Sender};
58
59static SENDER: Lazy<Sender<DiagEvent>> = Lazy::new(|| {
60 let (mut s, r) = broadcast(1024);
61
62 // When the buffer is full, drop the oldest message instead of rejecting
63 // the newest. For real-time state (peer mute/video status), the latest
64 // event is always more relevant than stale ones.
65 s.set_overflow(true);
66
67 // Create a background task that keeps a receiver active
68 // This prevents the channel from closing when there are no active receivers
69 #[cfg(target_arch = "wasm32")]
70 {
71 let mut receiver = r;
72 wasm_bindgen_futures::spawn_local(async move {
73 // Keep the receiver alive to prevent channel closure
74 // This receiver will consume messages but not process them
75 while (receiver.recv().await).is_ok() {
76 // Intentionally discard messages in the background receiver
77 // This keeps the channel open for other receivers
78 }
79 });
80 }
81
82 #[cfg(not(target_arch = "wasm32"))]
83 {
84 // For native targets, we could use tokio::spawn here if needed
85 // For now, just drop the receiver and let the channel close if no receivers
86 std::mem::drop(r);
87 }
88
89 s
90});
91
92/// Obtain a sender that can publish diagnostics events.
93pub fn global_sender() -> Sender<DiagEvent> {
94 SENDER.deref().clone()
95}
96
97/// Subscribe to the diagnostics stream. Each subscriber receives **all** future events.
98pub fn subscribe() -> Receiver<DiagEvent> {
99 SENDER.deref().new_receiver()
100}
101
102// === Helper utilities ===
103
104/// Current wall-clock time in milliseconds.
105#[cfg(not(target_arch = "wasm32"))]
106pub fn now_ms() -> u64 {
107 use std::time::{SystemTime, UNIX_EPOCH};
108 SystemTime::now()
109 .duration_since(UNIX_EPOCH)
110 .unwrap_or_default()
111 .as_millis() as u64
112}
113
114#[cfg(target_arch = "wasm32")]
115pub fn now_ms() -> u64 {
116 js_sys::Date::now() as u64
117}
118
119// === metric! helper macro ===
120
121/// Shorthand for constructing a [`Metric`].
122#[macro_export]
123macro_rules! metric {
124 ($name:expr, $value:expr) => {
125 $crate::Metric {
126 name: $name,
127 value: $crate::MetricValue::from($value),
128 }
129 };
130}
131
132// Implement `From` conversions so `metric!("fps", 30)` works for common types.
133impl From<i64> for MetricValue {
134 fn from(v: i64) -> Self {
135 MetricValue::I64(v)
136 }
137}
138impl From<u64> for MetricValue {
139 fn from(v: u64) -> Self {
140 MetricValue::U64(v)
141 }
142}
143impl From<f64> for MetricValue {
144 fn from(v: f64) -> Self {
145 MetricValue::F64(v)
146 }
147}
148impl From<&str> for MetricValue {
149 fn from(v: &str) -> Self {
150 MetricValue::Text(v.to_string())
151 }
152}
153impl From<String> for MetricValue {
154 fn from(v: String) -> Self {
155 MetricValue::Text(v)
156 }
157}