<div align="center">
<h1>mediatime</h1>
</div>
<div align="center">

Exact-integer rational time types for media pipelines — FFmpeg-style `Timebase`, `Timestamp`, and `TimeRange` for Rust. `no_std` by default, zero dependencies, `const fn` throughout.

[<img alt="github" src="https://img.shields.io/badge/github-findit--ai/mediatime-8da0cb?style=for-the-badge&logo=Github" height="22">][Github-url]
<img alt="LoC" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fal8n%2F327b2a8aef9003246e45c6e47fe63937%2Fraw%2Fmediatime" height="22">
[<img alt="Build" src="https://img.shields.io/github/actions/workflow/status/findit-ai/mediatime/ci.yml?logo=Github-Actions&style=for-the-badge" height="22">][CI-url]
[<img alt="codecov" src="https://img.shields.io/codecov/c/gh/findit-ai/mediatime?style=for-the-badge&token=6R3QFWRWHL&logo=codecov" height="22">][codecov-url]

[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-mediatime-66c2a5?style=for-the-badge&labelColor=555555&logo=data:image/svg+xml;base64, " height="20">][doc-url]
[<img alt="crates.io" src="https://img.shields.io/crates/v/mediatime?style=for-the-badge&logo=data:image/svg+xml;base64, " height="22">][crates-url]
[<img alt="crates.io" src="https://img.shields.io/crates/d/mediatime?color=critical&logo=data:image/svg+xml;base64, &style=for-the-badge" height="22">][crates-url]
<img alt="license" src="https://img.shields.io/badge/License-Apache%202.0/MIT-blue.svg?style=for-the-badge&fontColor=white&logoColor=f5c076&logo=data:image/svg+xml;base64, " height="22">

</div>

## Overview

`mediatime` provides the same three primitives every media pipeline reinvents, done once with integer-exact semantics:

- **[`Timebase`]** — a rational `num/den` (both `u32`, non-zero denominator). Directly mirrors FFmpeg's `AVRational`. Common values: `1/1000` (ms PTS), `1/90000` (MPEG-TS), `30000/1001` (NTSC frame rate).
- **[`Timestamp`]** — an `i64` PTS tagged with a `Timebase`. Two timestamps compare by the *instant* they represent, not by their raw `(pts, timebase)` tuple, so `Timestamp(1_000, 1/1000)` equals `Timestamp(90_000, 1/90_000)`. Cross-timebase comparison uses a 128-bit cross-multiply — no division, no rounding.
- **[`TimeRange`]** — a half-open `[start, end)` interval sharing a single `Timebase`. Carries the endpoints as raw PTS; returns `Timestamp` on demand.

Everything is `const fn`. The crate only uses `core` — no allocation, no dependencies. Use it as the time layer for scene detectors, demuxers, NLE timelines, or anywhere you'd otherwise pass `f64` seconds around and pay for rounding drift later.

[`Timebase`]: https://docs.rs/mediatime/latest/mediatime/struct.Timebase.html
[`Timestamp`]: https://docs.rs/mediatime/latest/mediatime/struct.Timestamp.html
[`TimeRange`]: https://docs.rs/mediatime/latest/mediatime/struct.TimeRange.html

## Why not `f64` seconds?

Floating-point seconds accumulate drift: `0.1 + 0.2 != 0.3`. Real video timestamps are already integer PTS in an integer timebase — converting to `f64` for arithmetic only to convert back on output *introduces* rounding error. `mediatime` keeps the representation that the stream actually carries, and does exact rational arithmetic on it.

Equality semantics show the win:

```text
f64 seconds:              0.1 + 0.2 == 0.3       → false
mediatime::Timestamp:     100 ms    == 9000 ticks @ 1/90000 → true
```

## Features

- **Value-based equality and ordering.** `1/2 == 2/4 == 3/6`; `Timestamp(1000, 1/1000) == Timestamp(90_000, 1/90_000)`. Cross-timebase `cmp` uses 128-bit cross-multiply — exact for any `u32` numerator/denominator with any `i64` PTS.
- **Hash agrees with Eq.** Hashes the reduced-form rational, so equal rationals hash identically and you can use these types as `HashMap` keys.
- **FFmpeg-style utilities.** `rescale_pts` (a.k.a. `av_rescale_q`), `frames_to_duration`, `duration_to_pts`, `duration_since`, `saturating_sub_duration`.
- **`TimeRange` interpolation.** Linear midpoint (`interpolate(t)`) for placing an event somewhere between fade-out and fade-in frames, with `t ∈ [0, 1]` clamped.
- **`no_std` + `no_alloc` library.** The library builds without `std` and `alloc`; tests use `std`.
- **`const fn` throughout.** Build `Timebase` / `Timestamp` / `TimeRange` in `const` context.

## Example

```rust
use core::num::NonZeroU32;
use core::time::Duration;
use mediatime::{Timebase, Timestamp, TimeRange};

// FFmpeg-style rational timebases.
let ms     = Timebase::new(1, NonZeroU32::new(1000).unwrap());
let mpegts = Timebase::new(1, NonZeroU32::new(90_000).unwrap());

// Same instant in two different timebases — they compare equal.
let a = Timestamp::new(1_000, ms);
let b = Timestamp::new(90_000, mpegts);
assert_eq!(a, b);
assert_eq!(a.duration_since(&b), Some(Duration::ZERO));

// `av_rescale_q`-style conversion, rounding toward zero.
assert_eq!(ms.rescale(500, mpegts), 45_000);

// Frame rate helpers — treat `Timebase` as fps and count frames.
let ntsc = Timebase::new(30_000, NonZeroU32::new(1001).unwrap());
assert_eq!(ntsc.frames_to_duration(30_000), Duration::from_secs(1001));

// A half-open [start, end) range with interpolation.
let r = TimeRange::new(100, 500, ms);
assert_eq!(r.interpolate(0.5).pts(), 300);
assert_eq!(r.duration(), Some(Duration::from_millis(400)));
```

## Installation

```toml
[dependencies]
mediatime = "0.1"
```

## MSRV

Rust 1.85.

#### License

`mediatime` is under the terms of both the MIT license and the
Apache License (Version 2.0).

See [LICENSE-APACHE](LICENSE-APACHE), [LICENSE-MIT](LICENSE-MIT) for details.

Copyright (c) 2026 FinDIT Studio authors.

[Github-url]: https://github.com/findit-ai/mediatime/
[CI-url]: https://github.com/findit-ai/mediatime/actions/workflows/ci.yml
[doc-url]: https://docs.rs/mediatime
[crates-url]: https://crates.io/crates/mediatime
[codecov-url]: https://app.codecov.io/gh/findit-ai/mediatime/
