1use std::sync::Mutex;
37use std::time::Instant;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub struct MediaTime {
42 nanos: i64,
43}
44
45impl MediaTime {
46 pub const ZERO: Self = Self { nanos: 0 };
48
49 #[must_use]
51 pub const fn from_nanos(nanos: i64) -> Self {
52 Self { nanos }
53 }
54
55 #[must_use]
57 pub fn from_seconds(s: f64) -> Self {
58 Self {
59 nanos: nanos_from_seconds(s),
60 }
61 }
62
63 #[must_use]
67 pub fn from_sample(index: u64, sample_rate: u32) -> Self {
68 assert!(sample_rate > 0, "sample_rate must be > 0");
69 let numerator = u128::from(index) * 1_000_000_000_u128;
72 let nanos = (numerator / u128::from(sample_rate))
73 .try_into()
74 .expect("sample timestamp overflows i64 nanoseconds");
75 Self { nanos }
76 }
77
78 #[must_use]
82 pub fn from_frame(index: u64, frame_rate: f64) -> Self {
83 assert!(
84 frame_rate.is_finite() && frame_rate > 0.0,
85 "frame_rate must be finite and > 0"
86 );
87 Self::from_seconds(index_as_f64(index) / frame_rate)
88 }
89
90 #[must_use]
92 pub const fn as_nanos(self) -> i64 {
93 self.nanos
94 }
95
96 #[must_use]
98 pub fn as_seconds(self) -> f64 {
99 #[expect(
103 clippy::cast_precision_loss,
104 reason = "f64 covers all reasonable session lengths; clients wanting exact nanos use as_nanos()"
105 )]
106 let secs = self.nanos as f64 / 1.0e9;
107 secs
108 }
109
110 #[must_use]
119 pub fn to_sample(self, sample_rate: u32) -> i64 {
120 assert!(sample_rate > 0, "sample_rate must be > 0");
121 let product = i128::from(self.nanos) * i128::from(sample_rate);
125 let half: i128 = 500_000_000;
126 let biased = if product >= 0 {
127 product + half
128 } else {
129 product - half
130 };
131 (biased / 1_000_000_000_i128)
132 .try_into()
133 .expect("sample index overflows i64")
134 }
135
136 #[must_use]
140 pub fn to_frame(self, frame_rate: f64) -> i64 {
141 assert!(
142 frame_rate.is_finite() && frame_rate > 0.0,
143 "frame_rate must be finite and > 0"
144 );
145 let seconds = self.as_seconds();
146 #[expect(
147 clippy::cast_possible_truncation,
148 reason = "frame index from valid timestamp + fps fits in i64 for any reasonable session"
149 )]
150 let idx = (seconds * frame_rate).round() as i64;
151 idx
152 }
153}
154
155impl std::ops::Add<MediaDuration> for MediaTime {
156 type Output = Self;
157 fn add(self, rhs: MediaDuration) -> Self {
158 Self {
159 nanos: self.nanos.saturating_add(rhs.nanos),
160 }
161 }
162}
163
164impl std::ops::Sub<MediaDuration> for MediaTime {
165 type Output = Self;
166 fn sub(self, rhs: MediaDuration) -> Self {
167 Self {
168 nanos: self.nanos.saturating_sub(rhs.nanos),
169 }
170 }
171}
172
173impl std::ops::Sub<MediaTime> for MediaTime {
174 type Output = MediaDuration;
175 fn sub(self, rhs: MediaTime) -> MediaDuration {
177 MediaDuration {
178 nanos: self.nanos.saturating_sub(rhs.nanos),
179 }
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
186pub struct MediaDuration {
187 nanos: i64,
188}
189
190impl MediaDuration {
191 pub const ZERO: Self = Self { nanos: 0 };
193
194 #[must_use]
196 pub const fn from_nanos(nanos: i64) -> Self {
197 Self { nanos }
198 }
199
200 #[must_use]
202 pub fn from_seconds(s: f64) -> Self {
203 Self {
204 nanos: nanos_from_seconds(s),
205 }
206 }
207
208 #[must_use]
210 pub const fn from_millis(ms: i64) -> Self {
211 Self {
212 nanos: ms.saturating_mul(1_000_000),
213 }
214 }
215
216 #[must_use]
218 pub const fn as_nanos(self) -> i64 {
219 self.nanos
220 }
221
222 #[must_use]
224 pub fn as_seconds(self) -> f64 {
225 #[expect(
226 clippy::cast_precision_loss,
227 reason = "callers wanting exact nanos use as_nanos()"
228 )]
229 let secs = self.nanos as f64 / 1.0e9;
230 secs
231 }
232
233 #[must_use]
235 pub const fn abs(self) -> Self {
236 Self {
237 nanos: self.nanos.abs(),
238 }
239 }
240}
241
242impl std::ops::Add<MediaDuration> for MediaDuration {
243 type Output = Self;
244 fn add(self, rhs: Self) -> Self {
245 Self {
246 nanos: self.nanos.saturating_add(rhs.nanos),
247 }
248 }
249}
250
251impl std::ops::Sub<MediaDuration> for MediaDuration {
252 type Output = Self;
253 fn sub(self, rhs: Self) -> Self {
254 Self {
255 nanos: self.nanos.saturating_sub(rhs.nanos),
256 }
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
266pub struct Timestamped<T> {
267 pub time: MediaTime,
269 pub value: T,
271}
272
273impl<T> Timestamped<T> {
274 pub const fn new(time: MediaTime, value: T) -> Self {
276 Self { time, value }
277 }
278
279 pub const fn as_ref(&self) -> Timestamped<&T> {
281 Timestamped {
282 time: self.time,
283 value: &self.value,
284 }
285 }
286}
287
288#[derive(Debug)]
300pub struct MediaClock {
301 inner: ClockInner,
302}
303
304#[derive(Debug)]
305enum ClockInner {
306 Wall { origin: Instant },
307 Manual { current: Mutex<MediaTime> },
308}
309
310impl Default for MediaClock {
311 fn default() -> Self {
312 Self::wall_clock()
313 }
314}
315
316impl MediaClock {
317 #[must_use]
321 pub fn wall_clock() -> Self {
322 Self {
323 inner: ClockInner::Wall {
324 origin: Instant::now(),
325 },
326 }
327 }
328
329 #[must_use]
333 pub fn manual(start: MediaTime) -> Self {
334 Self {
335 inner: ClockInner::Manual {
336 current: Mutex::new(start),
337 },
338 }
339 }
340
341 #[must_use]
343 pub fn now(&self) -> MediaTime {
344 match &self.inner {
345 ClockInner::Wall { origin } => {
346 let elapsed = origin.elapsed();
347 let nanos = i64::try_from(elapsed.as_nanos())
348 .expect("wall-clock elapsed overflows i64 ns (session > 292y)");
349 MediaTime::from_nanos(nanos)
350 }
351 ClockInner::Manual { current } => *current.lock().expect("clock poisoned"),
352 }
353 }
354
355 pub fn advance_by(&self, duration: MediaDuration) {
357 if let ClockInner::Manual { current } = &self.inner {
358 let mut t = current.lock().expect("clock poisoned");
359 *t = *t + duration;
360 }
361 }
362
363 pub fn assign<T>(&self, value: T) -> Timestamped<T> {
365 Timestamped::new(self.now(), value)
366 }
367
368 #[must_use]
371 pub const fn is_manual(&self) -> bool {
372 matches!(self.inner, ClockInner::Manual { .. })
373 }
374}
375
376fn nanos_from_seconds(s: f64) -> i64 {
377 assert!(s.is_finite(), "seconds must be finite");
378 #[expect(
379 clippy::cast_possible_truncation,
380 reason = "i64 nanos covers ±292y — any realistic recorder session"
381 )]
382 let nanos = (s * 1.0e9).round() as i64;
383 nanos
384}
385
386fn index_as_f64(index: u64) -> f64 {
387 #[expect(
388 clippy::cast_precision_loss,
389 reason = "frame indices above 2^53 (≈ 9.4e15) are not realistic for recorder sessions"
390 )]
391 let n = index as f64;
392 n
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn frame_90_at_30fps_equals_three_seconds() {
401 let t = MediaTime::from_frame(90, 30.0);
402 assert!(
403 (t.as_seconds() - 3.0).abs() < 1e-9,
404 "got {} s",
405 t.as_seconds()
406 );
407 }
408
409 #[test]
410 fn sample_48000_at_48khz_equals_one_second() {
411 let t = MediaTime::from_sample(48_000, 48_000);
412 assert!(
413 (t.as_seconds() - 1.0).abs() < 1e-12,
414 "got {} s ({} ns)",
415 t.as_seconds(),
416 t.as_nanos()
417 );
418 assert_eq!(t.as_nanos(), 1_000_000_000);
420 }
421
422 #[test]
423 fn sample_round_trip_is_exact() {
424 for (idx, rate) in [
426 (0_u64, 48_000_u32),
427 (1, 44_100),
428 (48_000, 48_000),
429 (1_234_567, 48_000),
430 ] {
431 let t = MediaTime::from_sample(idx, rate);
432 assert_eq!(
433 t.to_sample(rate),
434 i64::try_from(idx).unwrap(),
435 "round-trip failed for idx={idx} rate={rate}"
436 );
437 }
438 }
439
440 #[test]
441 fn frame_round_trip_is_exact_for_common_rates() {
442 for (idx, fps) in [(0_u64, 30.0), (1, 30.0), (90, 30.0), (60, 60.0), (24, 24.0)] {
443 let t = MediaTime::from_frame(idx, fps);
444 assert_eq!(
445 t.to_frame(fps),
446 i64::try_from(idx).unwrap(),
447 "round-trip failed for idx={idx} fps={fps}"
448 );
449 }
450 }
451
452 #[test]
453 fn duration_arith_seconds() {
454 let a = MediaDuration::from_seconds(1.5);
455 let b = MediaDuration::from_seconds(0.5);
456 assert!((((a + b).as_seconds()) - 2.0).abs() < 1e-9);
457 assert!((((a - b).as_seconds()) - 1.0).abs() < 1e-9);
458 }
459
460 #[test]
461 fn duration_arith_via_media_time() {
462 let t0 = MediaTime::from_seconds(1.0);
463 let t1 = MediaTime::from_seconds(4.25);
464 let d = t1 - t0;
465 assert!((d.as_seconds() - 3.25).abs() < 1e-9);
466 assert_eq!((t0 + d), t1);
467 }
468
469 #[test]
470 fn media_time_is_monotonically_ordered() {
471 let a = MediaTime::from_seconds(1.0);
472 let b = MediaTime::from_seconds(1.000_000_001);
473 let c = MediaTime::from_seconds(2.0);
474 assert!(a < b);
475 assert!(b < c);
476 assert!(a < c);
477 let mut v = vec![c, a, b];
478 v.sort();
479 assert_eq!(v, vec![a, b, c]);
480 }
481
482 #[test]
483 fn manual_clock_only_advances_when_told() {
484 let c = MediaClock::manual(MediaTime::ZERO);
485 assert_eq!(c.now(), MediaTime::ZERO);
486 c.advance_by(MediaDuration::from_seconds(0.5));
487 assert!((c.now().as_seconds() - 0.5).abs() < 1e-12);
488 c.advance_by(MediaDuration::from_seconds(0.5));
489 assert!((c.now().as_seconds() - 1.0).abs() < 1e-12);
490 assert!(c.is_manual());
491 }
492
493 #[test]
494 fn wall_clock_is_monotonically_non_decreasing() {
495 let c = MediaClock::wall_clock();
496 let mut last = c.now();
497 for _ in 0..10 {
498 let t = c.now();
499 assert!(t >= last, "wall clock went backwards");
500 last = t;
501 }
502 assert!(!c.is_manual());
503 }
504
505 #[test]
506 fn timestamped_carries_value() {
507 let c = MediaClock::manual(MediaTime::from_seconds(2.5));
508 let ts: Timestamped<&str> = c.assign("hello");
509 assert_eq!(ts.value, "hello");
510 assert!((ts.time.as_seconds() - 2.5).abs() < 1e-12);
511 }
512
513 #[test]
514 fn duration_abs_for_drift_reporting() {
515 let d = MediaDuration::from_seconds(-0.03);
516 assert!((d.abs().as_seconds() - 0.03).abs() < 1e-9);
517 }
518
519 #[test]
520 fn duration_from_millis_is_exact() {
521 let d = MediaDuration::from_millis(20);
522 assert_eq!(d.as_nanos(), 20_000_000);
523 }
524
525 #[test]
526 fn types_are_send_and_sync() {
527 fn assert_send_sync<T: Send + Sync>() {}
528 assert_send_sync::<MediaTime>();
529 assert_send_sync::<MediaDuration>();
530 assert_send_sync::<MediaClock>();
531 assert_send_sync::<Timestamped<u32>>();
532 }
533}