use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use crate::context::{AudioContextState, BaseAudioContext, ConcreteBaseAudioContext};
use crate::stats::AudioStats;
const UPDATE_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Clone, Debug, Default)]
pub struct AudioPlaybackStatsSnapshot {
pub underrun_duration: f64,
pub underrun_events: u64,
pub total_duration: f64,
pub average_latency: f64,
pub minimum_latency: f64,
pub maximum_latency: f64,
}
#[derive(Debug, Default)]
struct ExposedStats {
values: AudioPlaybackStatsSnapshot,
last_update: Option<Instant>,
}
#[derive(Clone, Debug)]
pub struct AudioPlaybackStats {
context: ConcreteBaseAudioContext,
stats: AudioStats,
exposed: Arc<Mutex<ExposedStats>>,
}
impl AudioPlaybackStats {
pub(crate) fn new(context: ConcreteBaseAudioContext, stats: AudioStats) -> Self {
let instance = Self {
context,
stats,
exposed: Arc::new(Mutex::new(ExposedStats::default())),
};
instance.stats.reset_latency();
instance
}
#[must_use]
pub fn underrun_duration(&self) -> f64 {
self.current().underrun_duration
}
#[must_use]
pub fn underrun_events(&self) -> u64 {
self.current().underrun_events
}
#[must_use]
pub fn total_duration(&self) -> f64 {
self.current().total_duration
}
#[must_use]
pub fn average_latency(&self) -> f64 {
self.current().average_latency
}
#[must_use]
pub fn minimum_latency(&self) -> f64 {
self.current().minimum_latency
}
#[must_use]
pub fn maximum_latency(&self) -> f64 {
self.current().maximum_latency
}
pub fn reset_latency(&self) {
self.stats.reset_latency();
self.refresh();
}
#[must_use]
pub fn to_json(&self) -> AudioPlaybackStatsSnapshot {
self.current()
}
fn current(&self) -> AudioPlaybackStatsSnapshot {
let mut exposed = self.exposed.lock().unwrap();
let should_update = self.context.state() == AudioContextState::Running
&& exposed
.last_update
.is_none_or(|last_update| last_update.elapsed() >= UPDATE_INTERVAL);
if should_update {
exposed.values = self.read_current_values();
exposed.last_update = Some(Instant::now());
}
exposed.values.clone()
}
fn refresh(&self) {
let mut exposed = self.exposed.lock().unwrap();
exposed.values = self.read_current_values();
exposed.last_update = Some(Instant::now());
}
fn read_current_values(&self) -> AudioPlaybackStatsSnapshot {
let snapshot = self.stats.snapshot();
let underrun_duration = snapshot.underrun_duration_seconds();
AudioPlaybackStatsSnapshot {
underrun_duration,
underrun_events: snapshot.underrun_events_total,
total_duration: underrun_duration + self.context.current_time(),
average_latency: snapshot.average_latency_seconds(),
minimum_latency: snapshot.minimum_latency_seconds(),
maximum_latency: snapshot.maximum_latency_seconds(),
}
}
}
#[cfg(test)]
mod tests {
use crate::context::{AudioContext, AudioContextOptions};
use std::sync::Arc;
use std::time::Duration;
#[test]
fn test_same_instance() {
let options = AudioContextOptions {
sink_id: "none".into(),
..AudioContextOptions::default()
};
let context = AudioContext::new(options);
let stats1 = context.playback_stats();
let stats2 = context.playback_stats();
let stats3 = stats2.clone();
assert!(Arc::ptr_eq(&stats1.exposed, &stats2.exposed));
assert!(Arc::ptr_eq(&stats1.exposed, &stats3.exposed));
}
#[test]
fn test_playback_stats() {
let options = AudioContextOptions {
sink_id: "none".into(),
..AudioContextOptions::default()
};
let context = AudioContext::new(options);
let stats = context.playback_stats();
std::thread::sleep(Duration::from_millis(50));
let snapshot = stats.to_json();
assert!(snapshot.total_duration > 0.);
assert!(snapshot.underrun_duration >= 0.);
assert_eq!(stats.underrun_events(), snapshot.underrun_events);
assert!(stats.average_latency().is_finite());
assert!(stats.minimum_latency().is_finite());
assert!(stats.maximum_latency().is_finite());
stats.reset_latency();
assert_eq!(stats.average_latency(), 0.);
assert_eq!(stats.minimum_latency(), 0.);
assert_eq!(stats.maximum_latency(), 0.);
}
#[test]
fn test_playback_stats_do_not_update_when_closed() {
let options = AudioContextOptions {
sink_id: "none".into(),
..AudioContextOptions::default()
};
let context = AudioContext::new(options);
let stats = context.playback_stats();
std::thread::sleep(Duration::from_millis(50));
let running_total_duration = stats.total_duration();
context.close_sync();
std::thread::sleep(Duration::from_secs(2));
assert_eq!(stats.total_duration(), running_total_duration);
}
}