web_audio_api/
playback_stats.rs1use 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#[derive(Clone, Debug, Default)]
11pub struct AudioPlaybackStatsSnapshot {
12 pub underrun_duration: f64,
14 pub underrun_events: u64,
16 pub total_duration: f64,
18 pub average_latency: f64,
20 pub minimum_latency: f64,
22 pub maximum_latency: f64,
24}
25
26#[derive(Debug, Default)]
27struct ExposedStats {
28 values: AudioPlaybackStatsSnapshot,
29 last_update: Option<Instant>,
30}
31
32#[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 #[must_use]
57 pub fn underrun_duration(&self) -> f64 {
58 self.current().underrun_duration
59 }
60
61 #[must_use]
63 pub fn underrun_events(&self) -> u64 {
64 self.current().underrun_events
65 }
66
67 #[must_use]
69 pub fn total_duration(&self) -> f64 {
70 self.current().total_duration
71 }
72
73 #[must_use]
75 pub fn average_latency(&self) -> f64 {
76 self.current().average_latency
77 }
78
79 #[must_use]
81 pub fn minimum_latency(&self) -> f64 {
82 self.current().minimum_latency
83 }
84
85 #[must_use]
87 pub fn maximum_latency(&self) -> f64 {
88 self.current().maximum_latency
89 }
90
91 pub fn reset_latency(&self) {
93 self.stats.reset_latency();
94 self.refresh();
95 }
96
97 #[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}