use std::collections::HashMap;
use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum Role {
Main,
Audio,
}
impl Role {
fn other(self) -> Role {
match self {
Role::Main => Role::Audio,
Role::Audio => Role::Main,
}
}
}
pub(crate) struct Timeline {
offsets: HashMap<(Role, u64), i128>,
tolerance: Duration,
}
impl Timeline {
pub fn new(tolerance: Duration) -> Self {
Self { offsets: HashMap::new(), tolerance }
}
pub fn anchor(&mut self, role: Role, disc: u64, start: Duration, first_raw: Duration) -> i128 {
if let Some(&o) = self.offsets.get(&(role, disc)) {
return o;
}
let own = start.as_nanos() as i128 - first_raw.as_nanos() as i128;
let offset = match self.offsets.get(&(role.other(), disc)) {
Some(&o) if (own - o).unsigned_abs() <= self.tolerance.as_nanos() => o,
_ => own,
};
self.offsets.insert((role, disc), offset);
offset
}
pub fn reanchor(&mut self, role: Role, disc: u64, start: Duration, first_raw: Duration) -> i128 {
self.forget(role);
let own = start.as_nanos() as i128 - first_raw.as_nanos() as i128;
self.offsets.insert((role, disc), own);
own
}
pub fn offset(&self, role: Role, disc: u64) -> Option<i128> {
self.offsets.get(&(role, disc)).copied()
}
pub fn forget(&mut self, role: Role) {
self.offsets.retain(|(r, _), _| *r != role);
}
pub fn map(offset: i128, raw: Duration) -> Duration {
let t = raw.as_nanos() as i128 + offset;
Duration::from_nanos(t.clamp(0, u64::MAX as i128) as u64)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(secs: f64) -> Duration {
Duration::from_secs_f64(secs)
}
#[test]
fn a_sequence_keeps_its_first_offset() {
let mut t = Timeline::new(s(1.0));
let o = t.anchor(Role::Main, 0, s(0.0), s(10.0));
assert_eq!(Timeline::map(o, s(10.5)), s(0.5));
let o2 = t.anchor(Role::Main, 0, s(4.0), s(14.02));
assert_eq!(o2, o);
assert_eq!(Timeline::map(o2, s(14.02)), s(4.02));
}
#[test]
fn a_discontinuity_reanchors() {
let mut t = Timeline::new(s(1.0));
t.anchor(Role::Main, 0, s(0.0), s(10.0));
let o = t.anchor(Role::Main, 1, s(3.0), s(1.4));
assert_eq!(Timeline::map(o, s(1.4)), s(3.0));
}
#[test]
fn aligned_audio_reuses_the_video_offset() {
let mut t = Timeline::new(s(1.0));
let v = t.anchor(Role::Main, 0, s(0.0), s(10.0));
let a = t.anchor(Role::Audio, 0, s(0.0), s(9.979));
assert_eq!(a, v, "same clock: sample-exact sync");
assert_eq!(Timeline::map(a, s(9.979)), Duration::ZERO, "clamped at 0");
}
#[test]
fn unrelated_clocks_use_their_own_offset() {
let mut t = Timeline::new(s(1.0));
t.anchor(Role::Main, 0, s(0.0), s(10.0));
let a = t.anchor(Role::Audio, 0, s(0.0), s(500.0));
assert_eq!(Timeline::map(a, s(500.0)), Duration::ZERO);
}
}