use std::collections::VecDeque;
use std::time::Duration;
use mediaway_common::{Bytes, Packet, Rational};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ReplayError {
#[error("stream {stream_id} was not added to the replay ring")]
UnknownStream {
stream_id: u32,
},
#[error("stream {stream_id} is already in the replay ring")]
DuplicateStream {
stream_id: u32,
},
#[error("stream {stream_id} has a degenerate timebase {num}/{den}")]
BadTimebase {
stream_id: u32,
num: u64,
den: u32,
},
#[error("stream {stream_id}: dts {got} after {previous}; packets must arrive in decode order")]
OutOfOrder {
stream_id: u32,
previous: i64,
got: i64,
},
}
pub trait ReplayPayload {
fn byte_len(&self) -> usize;
}
impl ReplayPayload for Bytes {
fn byte_len(&self) -> usize {
self.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StoredPayload {
pub file: u32,
pub offset: u64,
pub len: u32,
}
impl ReplayPayload for StoredPayload {
fn byte_len(&self) -> usize {
usize::try_from(self.len).unwrap_or(usize::MAX)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PacketMeta {
pub stream_id: u32,
pub pts: i64,
pub dts: i64,
pub duration: u64,
pub is_keyframe: bool,
pub is_discard: bool,
}
impl PacketMeta {
#[must_use]
pub const fn into_packet(self, payload: Bytes) -> Packet {
Packet {
stream_id: self.stream_id,
pts: self.pts,
dts: self.dts,
duration: self.duration,
is_keyframe: self.is_keyframe,
is_discard: self.is_discard,
payload,
}
}
}
impl From<&Packet> for PacketMeta {
fn from(p: &Packet) -> Self {
Self {
stream_id: p.stream_id,
pts: p.pts,
dts: p.dts,
duration: p.duration,
is_keyframe: p.is_keyframe,
is_discard: p.is_discard,
}
}
}
type Nanos = i128;
const NANOS_PER_SEC: i128 = 1_000_000_000;
fn to_nanos(ticks: i64, timebase: Rational) -> Nanos {
(i128::from(ticks) * i128::from(timebase.num) * NANOS_PER_SEC)
.div_euclid(i128::from(timebase.den))
}
fn to_ticks(nanos: Nanos, timebase: Rational) -> i64 {
let scale = i128::from(timebase.num) * NANOS_PER_SEC;
let ticks = (nanos * i128::from(timebase.den) + scale / 2).div_euclid(scale);
i64::try_from(ticks).unwrap_or(if ticks < 0 { i64::MIN } else { i64::MAX })
}
fn duration_nanos(d: Duration) -> Nanos {
Nanos::try_from(d.as_nanos()).unwrap_or(Nanos::MAX)
}
#[derive(Debug)]
struct Entry<P> {
meta: PacketMeta,
payload: P,
}
#[derive(Debug)]
struct Track<P> {
id: u32,
timebase: Rational,
entries: VecDeque<Entry<P>>,
}
impl<P> Track<P> {
fn last_dts(&self) -> Option<i64> {
self.entries.back().map(|e| e.meta.dts)
}
}
#[derive(Debug, Clone, Copy)]
struct CutPoint {
dts: i64,
pts: i64,
}
#[derive(Debug)]
pub struct ReplayRing<P = Bytes> {
tracks: Vec<Track<P>>,
cuts: VecDeque<CutPoint>,
window: Nanos,
max_bytes: Option<usize>,
bytes: usize,
newest: Option<Nanos>,
}
impl ReplayRing {
pub fn new(anchor: u32, timebase: Rational, window: Duration) -> Result<Self, ReplayError> {
Self::for_payload(anchor, timebase, window)
}
pub fn push(&mut self, packet: Packet) -> Result<(), ReplayError> {
let meta = PacketMeta::from(&packet);
self.push_entry(meta, packet.payload)
}
}
impl<P: ReplayPayload> ReplayRing<P> {
pub fn for_payload(
anchor: u32,
timebase: Rational,
window: Duration,
) -> Result<Self, ReplayError> {
let mut ring = Self {
tracks: Vec::with_capacity(2),
cuts: VecDeque::new(),
window: duration_nanos(window),
max_bytes: None,
bytes: 0,
newest: None,
};
ring.add_stream(anchor, timebase)?;
Ok(ring)
}
#[must_use]
pub const fn with_max_bytes(mut self, max_bytes: usize) -> Self {
self.max_bytes = Some(max_bytes);
self
}
pub fn add_stream(&mut self, stream_id: u32, timebase: Rational) -> Result<(), ReplayError> {
if timebase.num == 0 || timebase.den == 0 {
return Err(ReplayError::BadTimebase {
stream_id,
num: timebase.num,
den: timebase.den,
});
}
if self.tracks.iter().any(|t| t.id == stream_id) {
return Err(ReplayError::DuplicateStream { stream_id });
}
self.tracks.push(Track {
id: stream_id,
timebase,
entries: VecDeque::new(),
});
Ok(())
}
pub fn push_entry(&mut self, packet: PacketMeta, payload: P) -> Result<(), ReplayError> {
let index = self
.tracks
.iter()
.position(|t| t.id == packet.stream_id)
.ok_or(ReplayError::UnknownStream {
stream_id: packet.stream_id,
})?;
let track = &mut self.tracks[index];
if let Some(previous) = track.last_dts()
&& packet.dts < previous
{
return Err(ReplayError::OutOfOrder {
stream_id: packet.stream_id,
previous,
got: packet.dts,
});
}
if index == 0 {
if packet.is_keyframe {
self.cuts.push_back(CutPoint {
dts: packet.dts,
pts: packet.pts,
});
} else if self.cuts.is_empty() {
return Ok(());
}
}
let at = to_nanos(packet.dts, track.timebase);
self.newest = Some(self.newest.map_or(at, |n| n.max(at)));
self.bytes += payload.byte_len();
track.entries.push_back(Entry {
meta: packet,
payload,
});
self.evict();
Ok(())
}
fn evict(&mut self) {
let Some(newest) = self.newest else { return };
let anchor = self.tracks[0].timebase;
while let Some(&next) = self.cuts.get(1) {
let out_of_window = newest - to_nanos(next.dts, anchor) >= self.window;
let over_budget = self.max_bytes.is_some_and(|max| self.bytes > max);
if !(out_of_window || over_budget) {
break;
}
self.cuts.pop_front();
let (anchor_track, others) = self.tracks.split_at_mut(1);
let anchor_track = &mut anchor_track[0];
while anchor_track
.entries
.front()
.is_some_and(|e| e.meta.dts < next.dts)
{
if let Some(e) = anchor_track.entries.pop_front() {
self.bytes -= e.payload.byte_len();
}
}
let start = to_nanos(next.pts, anchor);
for track in others {
let tb = track.timebase;
while track
.entries
.front()
.is_some_and(|e| to_nanos(e.meta.pts, tb) < start)
{
if let Some(e) = track.entries.pop_front() {
self.bytes -= e.payload.byte_len();
}
}
}
}
}
#[must_use]
pub const fn bytes(&self) -> usize {
self.bytes
}
pub fn payloads(&self) -> impl Iterator<Item = &P> + '_ {
self.tracks
.iter()
.flat_map(|t| t.entries.iter().map(|e| &e.payload))
}
#[must_use]
pub fn span(&self) -> Duration {
match (self.cuts.front(), self.newest) {
(Some(first), Some(newest)) => {
let nanos = newest - to_nanos(first.dts, self.tracks[0].timebase);
Duration::from_nanos(u64::try_from(nanos.max(0)).unwrap_or(u64::MAX))
}
_ => Duration::ZERO,
}
}
#[must_use]
pub fn clip_last(&self, span: Duration) -> Option<Clip<'_, P>> {
let newest = self.newest?;
let anchor = self.tracks[0].timebase;
let target = newest.saturating_sub(duration_nanos(span));
let later = self
.cuts
.partition_point(|c| to_nanos(c.dts, anchor) <= target);
let cut = *self.cuts.get(later.saturating_sub(1))?;
Some(Clip {
ring: self,
cut,
origin: to_nanos(cut.dts, anchor),
})
}
}
#[derive(Debug)]
pub struct Clip<'a, P = Bytes> {
ring: &'a ReplayRing<P>,
cut: CutPoint,
origin: Nanos,
}
impl<P> Clone for Clip<'_, P> {
fn clone(&self) -> Self {
*self
}
}
impl<P> Copy for Clip<'_, P> {}
impl<'a, P> Clip<'a, P> {
#[must_use]
pub const fn anchor_origin(&self) -> i64 {
self.cut.dts
}
#[must_use]
pub fn duration(&self) -> Duration {
let nanos = self.ring.newest.unwrap_or(self.origin) - self.origin;
Duration::from_nanos(u64::try_from(nanos.max(0)).unwrap_or(u64::MAX))
}
#[must_use]
pub fn entries(&self) -> ClipEntries<'a, P> {
let ring = self.ring;
let cut_pts = to_nanos(self.cut.pts, ring.tracks[0].timebase);
let cursors = ring
.tracks
.iter()
.enumerate()
.map(|(i, t)| {
if i == 0 {
t.entries.partition_point(|e| e.meta.dts < self.cut.dts)
} else {
t.entries
.partition_point(|e| to_nanos(e.meta.dts, t.timebase) < cut_pts)
}
})
.collect();
ClipEntries {
ring,
cut: self.cut,
origin: self.origin,
cursors,
}
}
}
impl<'a> Clip<'a> {
#[must_use]
pub fn packets(&self) -> ClipPackets<'a> {
ClipPackets {
entries: self.entries(),
}
}
}
#[derive(Debug)]
pub struct ClipEntries<'a, P = Bytes> {
ring: &'a ReplayRing<P>,
cut: CutPoint,
origin: Nanos,
cursors: Vec<usize>,
}
impl<'a, P> Iterator for ClipEntries<'a, P> {
type Item = (PacketMeta, &'a P);
fn next(&mut self) -> Option<Self::Item> {
let ring = self.ring;
loop {
let (track, entry) = ring
.tracks
.iter()
.enumerate()
.filter_map(|(i, t)| t.entries.get(self.cursors[i]).map(|e| (i, e)))
.min_by_key(|&(i, e)| (to_nanos(e.meta.dts, ring.tracks[i].timebase), i))?;
self.cursors[track] += 1;
if track == 0 && entry.meta.pts < self.cut.pts {
continue;
}
let timebase = ring.tracks[track].timebase;
let offset = if track == 0 {
self.cut.dts
} else {
to_ticks(self.origin, timebase)
};
let mut meta = entry.meta;
meta.pts -= offset;
meta.dts -= offset;
return Some((meta, &entry.payload));
}
}
}
#[derive(Debug)]
pub struct ClipPackets<'a> {
entries: ClipEntries<'a>,
}
impl Iterator for ClipPackets<'_> {
type Item = Packet;
fn next(&mut self) -> Option<Packet> {
let (meta, payload) = self.entries.next()?;
Some(meta.into_packet(payload.clone()))
}
}
#[cfg(test)]
#[path = "replay_tests.rs"]
mod tests;