Skip to main content

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}