#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PlaybackState {
#[default]
Stopped,
Playing,
Paused,
Buffering,
Ended,
}
impl PlaybackState {
pub fn is_active(&self) -> bool {
matches!(self, PlaybackState::Playing | PlaybackState::Buffering)
}
pub fn can_resume(&self) -> bool {
matches!(self, PlaybackState::Paused)
}
pub fn label(&self) -> &'static str {
match self {
PlaybackState::Stopped => "Stopped",
PlaybackState::Playing => "Playing",
PlaybackState::Paused => "Paused",
PlaybackState::Buffering => "Buffering",
PlaybackState::Ended => "Ended",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameVerdict {
Show,
Wait,
Drop,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MediaClock {
position: f64,
duration: f64,
rate: f64,
state: PlaybackState,
dropped: u64,
}
impl Default for MediaClock {
fn default() -> Self {
Self::new()
}
}
impl MediaClock {
pub fn new() -> Self {
Self { position: 0.0, duration: 0.0, rate: 1.0, state: PlaybackState::Stopped, dropped: 0 }
}
pub fn set_duration(&mut self, duration_secs: f64) {
self.duration =
if duration_secs.is_finite() && duration_secs > 0.0 { duration_secs } else { 0.0 };
}
pub fn duration(&self) -> f64 {
self.duration
}
pub fn rate(&self) -> f64 {
self.rate
}
pub fn set_rate(&mut self, rate: f64) -> f64 {
if rate.is_finite() && rate > 0.0 {
self.rate = rate;
}
self.rate
}
pub fn state(&self) -> PlaybackState {
self.state
}
pub fn play(&mut self) {
if self.state == PlaybackState::Ended {
self.position = 0.0;
self.dropped = 0;
}
self.state = PlaybackState::Playing;
}
pub fn pause(&mut self) {
if self.state == PlaybackState::Playing {
self.state = PlaybackState::Paused;
}
}
pub fn stop(&mut self) {
self.state = PlaybackState::Stopped;
self.position = 0.0;
self.dropped = 0;
}
pub fn seek(&mut self, position_secs: f64) {
if !position_secs.is_finite() {
return;
}
let upper = if self.duration > 0.0 { self.duration } else { f64::MAX };
self.position = position_secs.clamp(0.0, upper);
if self.duration > 0.0 && self.position >= self.duration {
self.state = PlaybackState::Ended;
}
}
pub fn tick(&mut self, delta_ms: u32) -> f64 {
if self.state != PlaybackState::Playing {
return self.position;
}
let advance = (delta_ms as f64 / 1000.0) * self.rate;
self.position += advance;
if self.duration > 0.0 && self.position >= self.duration {
self.position = self.duration;
self.state = PlaybackState::Ended;
}
self.position
}
pub fn position(&self) -> f64 {
self.position
}
pub fn verdict_for(&self, frame_pts: f64, tolerance: f64) -> FrameVerdict {
if !frame_pts.is_finite() {
return FrameVerdict::Drop;
}
let tolerance = if tolerance.is_finite() && tolerance >= 0.0 { tolerance } else { 0.0 };
let lead = frame_pts - self.position;
if lead > tolerance {
FrameVerdict::Wait
} else if -lead > tolerance {
FrameVerdict::Drop
} else {
FrameVerdict::Show
}
}
pub fn note_dropped(&mut self) {
self.dropped = self.dropped.saturating_add(1);
}
pub fn take_dropped(&mut self) -> u64 {
core::mem::take(&mut self.dropped)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_clock_advances_by_the_frames_delta() {
let mut clock = MediaClock::new();
clock.set_duration(10.0);
clock.play();
for _ in 0..60 {
clock.tick(16);
}
assert!((clock.position() - 0.96).abs() < 1e-9, "position was {}", clock.position());
}
#[test]
fn a_paused_clock_does_not_advance() {
let mut clock = MediaClock::new();
clock.set_duration(10.0);
clock.play();
clock.tick(16);
let paused_at = clock.position();
clock.pause();
for _ in 0..60 {
clock.tick(16);
}
assert_eq!(clock.position(), paused_at, "a still frame must not run media time on");
assert_eq!(clock.state(), PlaybackState::Paused);
}
#[test]
fn the_clock_stops_at_the_end_and_reports_ended() {
let mut clock = MediaClock::new();
clock.set_duration(1.0);
clock.play();
clock.tick(5_000);
assert_eq!(clock.position(), 1.0, "the position clamps at the duration");
assert_eq!(clock.state(), PlaybackState::Ended);
clock.tick(16);
assert_eq!(clock.position(), 1.0);
}
#[test]
fn play_after_the_end_rewinds() {
let mut clock = MediaClock::new();
clock.set_duration(1.0);
clock.play();
clock.tick(2_000);
assert_eq!(clock.state(), PlaybackState::Ended);
clock.play();
assert_eq!(clock.position(), 0.0, "play on a finished track means play it again");
assert_eq!(clock.state(), PlaybackState::Playing);
}
#[test]
fn a_frame_is_waited_for_shown_or_dropped_by_how_far_it_is_from_now() {
let mut clock = MediaClock::new();
clock.set_duration(10.0);
clock.play();
for _ in 0..25 {
clock.tick(16); }
assert!((clock.position() - 0.4).abs() < 1e-9);
assert_eq!(clock.verdict_for(0.4, 0.008), FrameVerdict::Show);
assert_eq!(clock.verdict_for(0.405, 0.008), FrameVerdict::Show);
assert_eq!(clock.verdict_for(0.395, 0.008), FrameVerdict::Show);
assert_eq!(clock.verdict_for(0.5, 0.008), FrameVerdict::Wait);
assert_eq!(clock.verdict_for(0.1, 0.008), FrameVerdict::Drop);
}
#[test]
fn an_unusable_timestamp_is_dropped() {
let clock = MediaClock::new();
assert_eq!(clock.verdict_for(f64::NAN, 0.008), FrameVerdict::Drop);
assert_eq!(clock.verdict_for(f64::INFINITY, 0.008), FrameVerdict::Drop);
}
#[test]
fn the_rate_scales_the_advance_and_refuses_zero() {
let mut clock = MediaClock::new();
clock.set_duration(100.0);
clock.play();
assert_eq!(clock.set_rate(2.0), 2.0);
clock.tick(1000);
assert!((clock.position() - 2.0).abs() < 1e-9, "double speed is two seconds per second");
assert_eq!(clock.set_rate(0.0), 2.0);
assert_eq!(clock.set_rate(-1.0), 2.0);
assert_eq!(clock.set_rate(f64::NAN), 2.0);
}
#[test]
fn an_unusable_duration_clamps_nothing_and_does_not_panic() {
let mut clock = MediaClock::new();
clock.set_duration(f64::NAN);
assert_eq!(clock.duration(), 0.0, "an unknown duration is not a NaN bound");
clock.play();
clock.tick(1000);
assert!(
(clock.position() - 1.0).abs() < 1e-9,
"with no duration the clock runs on rather than clamping against a NaN"
);
}
#[test]
fn drops_are_counted_and_taken() {
let mut clock = MediaClock::new();
clock.note_dropped();
clock.note_dropped();
assert_eq!(clock.take_dropped(), 2, "this period dropped two");
assert_eq!(clock.take_dropped(), 0, "and the count was taken, not read");
}
#[test]
fn seek_is_clamped_and_keeps_the_state() {
let mut clock = MediaClock::new();
clock.set_duration(10.0);
clock.play();
clock.seek(5.0);
assert_eq!(clock.position(), 5.0);
assert_eq!(clock.state(), PlaybackState::Playing, "seeking does not pause");
clock.seek(-3.0);
assert_eq!(clock.position(), 0.0, "clamped to the start");
clock.seek(99.0);
assert_eq!(clock.position(), 10.0, "clamped to the end");
assert_eq!(clock.state(), PlaybackState::Ended, "landing on the end is the end");
clock.play();
clock.seek(4.0);
clock.seek(f64::NAN);
assert_eq!(clock.position(), 4.0);
}
}