Skip to main content

web_audio_api/
playback_stats.rs

1use std::sync::{Arc, Mutex};
2use std::time::{Duration, Instant};
3
4use crate::context::{AudioContextState, BaseAudioContext, ConcreteBaseAudioContext};
5use crate::stats::AudioStats;
6
7const UPDATE_INTERVAL: Duration = Duration::from_secs(1);
8
9/// Snapshot of [`AudioPlaybackStats`] values.
10#[derive(Clone, Debug, Default)]
11pub struct AudioPlaybackStatsSnapshot {
12    /// Total duration of underrun frames in seconds.
13    pub underrun_duration: f64,
14    /// Number of underrun events.
15    pub underrun_events: u64,
16    /// Total playback duration in seconds.
17    pub total_duration: f64,
18    /// Average output latency in seconds since the last latency reset.
19    pub average_latency: f64,
20    /// Minimum output latency in seconds since the last latency reset.
21    pub minimum_latency: f64,
22    /// Maximum output latency in seconds since the last latency reset.
23    pub maximum_latency: f64,
24}
25
26#[derive(Debug, Default)]
27struct ExposedStats {
28    values: AudioPlaybackStatsSnapshot,
29    last_update: Option<Instant>,
30}
31
32/// Playback statistics for an [`AudioContext`](crate::context::AudioContext).
33///
34/// The underrun counters are currently based on render callbacks that miss their realtime
35/// deadline. Backends that expose device-level underrun counters can feed more precise values into
36/// the shared stats layer in the future.
37#[derive(Clone, Debug)]
38pub struct AudioPlaybackStats {
39    context: ConcreteBaseAudioContext,
40    stats: AudioStats,
41    exposed: Arc<Mutex<ExposedStats>>,
42}
43
44impl AudioPlaybackStats {
45    pub(crate) fn new(context: ConcreteBaseAudioContext, stats: AudioStats) -> Self {
46        let instance = Self {
47            context,
48            stats,
49            exposed: Arc::new(Mutex::new(ExposedStats::default())),
50        };
51        instance.stats.reset_latency();
52        instance
53    }
54
55    /// Total duration of underrun frames in seconds.
56    #[must_use]
57    pub fn underrun_duration(&self) -> f64 {
58        self.current().underrun_duration
59    }
60
61    /// Number of underrun events.
62    #[must_use]
63    pub fn underrun_events(&self) -> u64 {
64        self.current().underrun_events
65    }
66
67    /// Total playback duration in seconds.
68    #[must_use]
69    pub fn total_duration(&self) -> f64 {
70        self.current().total_duration
71    }
72
73    /// Average output latency in seconds since the last latency reset.
74    #[must_use]
75    pub fn average_latency(&self) -> f64 {
76        self.current().average_latency
77    }
78
79    /// Minimum output latency in seconds since the last latency reset.
80    #[must_use]
81    pub fn minimum_latency(&self) -> f64 {
82        self.current().minimum_latency
83    }
84
85    /// Maximum output latency in seconds since the last latency reset.
86    #[must_use]
87    pub fn maximum_latency(&self) -> f64 {
88        self.current().maximum_latency
89    }
90
91    /// Reset the tracked latency interval.
92    pub fn reset_latency(&self) {
93        self.stats.reset_latency();
94        self.refresh();
95    }
96
97    /// Return the currently exposed values as a plain Rust snapshot.
98    #[must_use]
99    pub fn to_json(&self) -> AudioPlaybackStatsSnapshot {
100        self.current()
101    }
102
103    fn current(&self) -> AudioPlaybackStatsSnapshot {
104        let mut exposed = self.exposed.lock().unwrap();
105        let should_update = self.context.state() == AudioContextState::Running
106            && exposed
107                .last_update
108                .is_none_or(|last_update| last_update.elapsed() >= UPDATE_INTERVAL);
109        if should_update {
110            exposed.values = self.read_current_values();
111            exposed.last_update = Some(Instant::now());
112        }
113        exposed.values.clone()
114    }
115
116    fn refresh(&self) {
117        let mut exposed = self.exposed.lock().unwrap();
118        exposed.values = self.read_current_values();
119        exposed.last_update = Some(Instant::now());
120    }
121
122    fn read_current_values(&self) -> AudioPlaybackStatsSnapshot {
123        let snapshot = self.stats.snapshot();
124        let underrun_duration = snapshot.underrun_duration_seconds();
125
126        AudioPlaybackStatsSnapshot {
127            underrun_duration,
128            underrun_events: snapshot.underrun_events_total,
129            total_duration: underrun_duration + self.context.current_time(),
130            average_latency: snapshot.average_latency_seconds(),
131            minimum_latency: snapshot.minimum_latency_seconds(),
132            maximum_latency: snapshot.maximum_latency_seconds(),
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use crate::context::{AudioContext, AudioContextOptions};
140    use std::sync::Arc;
141    use std::time::Duration;
142
143    #[test]
144    fn test_same_instance() {
145        let options = AudioContextOptions {
146            sink_id: "none".into(),
147            ..AudioContextOptions::default()
148        };
149        let context = AudioContext::new(options);
150
151        let stats1 = context.playback_stats();
152        let stats2 = context.playback_stats();
153        let stats3 = stats2.clone();
154
155        assert!(Arc::ptr_eq(&stats1.exposed, &stats2.exposed));
156        assert!(Arc::ptr_eq(&stats1.exposed, &stats3.exposed));
157    }
158
159    #[test]
160    fn test_playback_stats() {
161        let options = AudioContextOptions {
162            sink_id: "none".into(),
163            ..AudioContextOptions::default()
164        };
165        let context = AudioContext::new(options);
166        let stats = context.playback_stats();
167
168        std::thread::sleep(Duration::from_millis(50));
169
170        let snapshot = stats.to_json();
171        assert!(snapshot.total_duration > 0.);
172        assert!(snapshot.underrun_duration >= 0.);
173        assert_eq!(stats.underrun_events(), snapshot.underrun_events);
174        assert!(stats.average_latency().is_finite());
175        assert!(stats.minimum_latency().is_finite());
176        assert!(stats.maximum_latency().is_finite());
177
178        stats.reset_latency();
179        assert_eq!(stats.average_latency(), 0.);
180        assert_eq!(stats.minimum_latency(), 0.);
181        assert_eq!(stats.maximum_latency(), 0.);
182    }
183
184    #[test]
185    fn test_playback_stats_do_not_update_when_closed() {
186        let options = AudioContextOptions {
187            sink_id: "none".into(),
188            ..AudioContextOptions::default()
189        };
190        let context = AudioContext::new(options);
191        let stats = context.playback_stats();
192
193        std::thread::sleep(Duration::from_millis(50));
194        let running_total_duration = stats.total_duration();
195
196        context.close_sync();
197        std::thread::sleep(Duration::from_secs(2));
198
199        assert_eq!(stats.total_duration(), running_total_duration);
200    }
201}