use std::collections::VecDeque;
use std::sync::{Arc, OnceLock};
use std::task::Poll;
use std::time::{Duration, SystemTime};
use bytes::Bytes;
use moq_mux::timeline::Entry;
use super::Rendition;
use crate::Result;
const DEFAULT_SEGMENT: Duration = Duration::from_secs(4);
pub(crate) struct Producer {
state: kio::Producer<State>,
pub broadcast: OnceLock<moq_net::broadcast::Consumer>,
}
struct State {
entries: VecDeque<Entry>,
dropped: u64,
ended: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Row {
pub group: u64,
pub duration: f64,
pub pts: moq_net::Timestamp,
}
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub(crate) struct Window {
pub sequence: u64,
pub segments: Vec<Row>,
pub ended: bool,
}
enum Next {
Ready { row: Row, successor: Option<u64> },
Ended,
Pending,
}
impl State {
#[cfg_attr(not(feature = "server"), allow(dead_code))]
fn window(&self) -> Window {
let mut segments = Vec::with_capacity(self.entries.len());
for (entry, next) in self.entries.iter().zip(self.entries.iter().skip(1)) {
segments.push(row(entry, Some(next)));
}
if self.ended
&& let Some(last) = self.entries.back()
{
segments.push(Row {
group: last.group,
duration: self.final_duration(),
pts: last.pts,
});
}
Window {
sequence: self.dropped,
segments,
ended: self.ended,
}
}
fn final_duration(&self) -> f64 {
self.entries
.iter()
.zip(self.entries.iter().skip(1))
.map(|(entry, next)| {
Duration::from(next.pts)
.saturating_sub(Duration::from(entry.pts))
.as_secs_f64()
})
.fold(0.0f64, f64::max)
.max(DEFAULT_SEGMENT.as_secs_f64())
}
fn next_after(&self, after: Option<u64>) -> Next {
let mut iter = self.entries.iter().enumerate().filter(|(_, e)| match after {
Some(after) => e.group > after,
None => true,
});
let Some((index, entry)) = iter.next() else {
return if self.ended { Next::Ended } else { Next::Pending };
};
match self.entries.get(index + 1) {
Some(next) => Next::Ready {
row: row(entry, Some(next)),
successor: Some(next.group),
},
None if self.ended => Next::Ready {
row: Row {
group: entry.group,
duration: self.final_duration(),
pts: entry.pts,
},
successor: None,
},
None => Next::Pending,
}
}
}
impl Producer {
pub fn new() -> Self {
Self {
state: kio::Producer::new(State {
entries: VecDeque::new(),
dropped: 0,
ended: false,
}),
broadcast: OnceLock::new(),
}
}
pub fn push(&self, entry: Entry, window: Duration) {
let Ok(mut state) = self.state.write() else {
return;
};
if let Some(back_pts) = state.entries.back().map(|e| e.pts) {
if entry.pts == back_pts {
return;
}
if entry.pts < back_pts {
tracing::warn!("timeline jumped backwards; resetting the playlist window");
state.dropped += state.entries.len() as u64;
state.entries.clear();
}
}
state.entries.push_back(entry);
while state.entries.len() >= 3 {
let span =
Duration::from(state.entries.back().unwrap().pts).saturating_sub(Duration::from(state.entries[1].pts));
if span < window {
break;
}
state.entries.pop_front();
state.dropped += 1;
}
}
pub fn end(&self) {
if let Ok(mut state) = self.state.write() {
state.ended = true;
}
}
pub fn close(&self) {
let _ = self.state.close();
}
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub fn window(&self) -> Window {
self.state.read().window()
}
pub fn segment_groups(&self, group: u64) -> Option<(u64, Option<u64>)> {
let state = self.state.read();
let index = state.entries.iter().position(|e| e.group == group)?;
match state.entries.get(index + 1) {
Some(next) => Some((group, Some(next.group))),
None if state.ended => Some((group, None)),
None => None,
}
}
pub fn latest_group(&self) -> Option<u64> {
let state = self.state.read();
if state.ended {
return state.entries.back().map(|e| e.group);
}
state.entries.iter().rev().nth(1).map(|e| e.group)
}
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub fn is_playable(&self) -> bool {
let state = self.state.read();
state.ended || state.entries.len() >= 2
}
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub fn poll_playable(&self, waiter: &kio::Waiter) -> Poll<()> {
let poll = self.state.poll_ref(waiter, |state| {
if state.ended || state.entries.len() >= 2 {
Poll::Ready(())
} else {
Poll::Pending
}
});
match poll {
Poll::Ready(_) => Poll::Ready(()),
Poll::Pending => Poll::Pending,
}
}
pub fn subscribe(&self, rendition: Arc<Rendition>) -> Consumer {
Consumer {
state: self.state.consume(),
rendition,
after: None,
expected: None,
gap: false,
}
}
}
pub struct Segment {
pub group: u64,
pub media: Bytes,
pub duration: f64,
pub program_date_time: Option<SystemTime>,
pub discontinuity: bool,
}
pub struct Consumer {
state: kio::Consumer<State>,
rendition: Arc<Rendition>,
after: Option<u64>,
expected: Option<u64>,
gap: bool,
}
impl Consumer {
pub async fn init(&self) -> Result<Option<Bytes>> {
self.rendition.init().await
}
pub async fn next(&mut self) -> Result<Option<Segment>> {
loop {
let Some((row, successor)) = kio::wait(|waiter| self.poll_next(waiter)).await else {
return Ok(None);
};
let gap = self.gap || self.expected.is_some_and(|expected| expected != row.group);
match self.rendition.segment(row.group).await? {
Some(media) => {
self.after = Some(row.group);
self.expected = successor;
self.gap = false;
return Ok(Some(Segment {
group: row.group,
media,
duration: row.duration,
program_date_time: self.rendition.wall_clock(row.pts),
discontinuity: gap,
}));
}
None => {
self.after = Some(row.group);
self.expected = successor;
self.gap = true;
}
}
}
}
fn poll_next(&self, waiter: &kio::Waiter) -> Poll<Option<(Row, Option<u64>)>> {
let poll = self.state.poll(waiter, |state| match state.next_after(self.after) {
Next::Ready { row, successor } => Poll::Ready(Some((row, successor))),
Next::Ended => Poll::Ready(None),
Next::Pending => Poll::Pending,
});
match poll {
Poll::Ready(Ok(found)) => Poll::Ready(found),
Poll::Ready(Err(_)) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
fn row(entry: &Entry, next: Option<&Entry>) -> Row {
let duration = next
.map(|next| Duration::from(next.pts).saturating_sub(Duration::from(entry.pts)))
.unwrap_or(DEFAULT_SEGMENT);
Row {
group: entry.group,
duration: duration.as_secs_f64(),
pts: entry.pts,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(group: u64, pts_ms: u64) -> Entry {
Entry {
group,
pts: moq_net::Timestamp::from_millis(pts_ms).unwrap(),
ext: (),
}
}
#[test]
fn last_record_is_the_live_edge_until_ended() {
let live = Producer::new();
live.push(entry(0, 0), Duration::from_secs(30));
live.push(entry(1, 2_000), Duration::from_secs(30));
live.push(entry(2, 4_000), Duration::from_secs(30));
let window = live.window();
assert_eq!(window.sequence, 0);
assert!(!window.ended);
assert_eq!(window.segments.len(), 2, "the live-edge record is not listed");
assert_eq!(window.segments[0].group, 0);
assert_eq!(window.segments[0].duration, 2.0);
assert_eq!(window.segments[1].group, 1);
live.end();
let window = live.window();
assert!(window.ended);
assert_eq!(window.segments.len(), 3, "ending lists the final record");
assert_eq!(window.segments[2].duration, 4.0, "final duration falls back");
}
#[test]
fn window_evicts_and_advances_sequence() {
let live = Producer::new();
let window = Duration::from_secs(4);
for i in 0..6u64 {
live.push(entry(i, i * 2_000), window);
}
let snapshot = live.window();
assert!(snapshot.sequence > 0);
let span: f64 = snapshot.segments.iter().map(|s| s.duration).sum();
assert!(span >= 4.0);
assert_eq!(snapshot.segments.first().unwrap().group, snapshot.sequence);
}
#[test]
fn segment_groups_cover_record_gaps() {
let live = Producer::new();
let window = Duration::from_secs(30);
live.push(entry(0, 0), window);
live.push(entry(50, 1_000), window);
live.push(entry(100, 2_000), window);
assert_eq!(live.segment_groups(0), Some((0, Some(50))));
assert_eq!(live.segment_groups(50), Some((50, Some(100))));
assert_eq!(live.segment_groups(100), None);
assert_eq!(live.segment_groups(7), None);
live.end();
assert_eq!(live.segment_groups(100), Some((100, None)));
}
#[test]
fn backwards_jump_resets_the_window() {
let live = Producer::new();
let window = Duration::from_secs(30);
live.push(entry(0, 10_000), window);
live.push(entry(1, 12_000), window);
live.push(entry(2, 1_000), window);
let snapshot = live.window();
assert_eq!(snapshot.sequence, 2, "reset keeps the media sequence monotonic");
assert!(snapshot.segments.is_empty(), "only the live edge remains");
}
#[test]
fn non_advancing_record_is_skipped() {
let live = Producer::new();
let window = Duration::from_secs(30);
live.push(entry(0, 0), window);
live.push(entry(1, 2_000), window);
live.push(entry(2, 2_000), window);
live.push(entry(3, 4_000), window);
live.end();
let window = live.window();
let durations: Vec<f64> = window.segments.iter().map(|s| s.duration).collect();
assert!(!durations.contains(&0.0), "no zero-duration segment: {durations:?}");
assert_eq!(live.segment_groups(1), Some((1, Some(3))));
assert_eq!(live.segment_groups(2), None, "the skipped record is not a boundary");
}
#[test]
fn closing_before_end_loses_the_final_segment() {
let window = Duration::from_secs(30);
let clean = Producer::new();
clean.push(entry(0, 0), window);
clean.push(entry(1, 2_000), window);
clean.end();
clean.close();
assert!(
matches!(clean.state.read().next_after(Some(0)), Next::Ready { .. }),
"end() before close() finalizes the live edge"
);
let raced = Producer::new();
raced.push(entry(0, 0), window);
raced.push(entry(1, 2_000), window);
raced.close();
raced.end();
assert!(
matches!(raced.state.read().next_after(Some(0)), Next::Pending),
"end() is a no-op after close(), so the final segment never finalizes"
);
}
#[test]
fn final_segment_duration_matches_the_serve_path() {
let live = Producer::new();
let window = Duration::from_secs(30);
live.push(entry(0, 0), window);
live.push(entry(1, 6_000), window);
live.push(entry(2, 12_000), window);
live.end();
let served = live.window();
let last_served = served.segments.last().expect("a final segment");
assert_eq!(last_served.group, 2);
assert_eq!(last_served.duration, 6.0, "serve path estimates from the cadence");
let last_recorded = match live.state.read().next_after(Some(1)) {
Next::Ready { row, .. } => row,
_ => panic!("ending finalizes the live edge"),
};
assert_eq!(last_recorded.group, 2);
assert_eq!(
last_recorded.duration, last_served.duration,
"the cursor times the final segment like the serve path"
);
}
#[test]
fn next_after_walks_finalized_segments() {
let live = Producer::new();
let window = Duration::from_secs(30);
live.push(entry(0, 0), window);
live.push(entry(1, 2_000), window);
live.push(entry(2, 4_000), window);
let first = match live.state.read().next_after(None) {
Next::Ready { row, .. } => row,
_ => panic!("expected a finalized segment"),
};
assert_eq!(first.group, 0);
let second = match live.state.read().next_after(Some(0)) {
Next::Ready { row, .. } => row,
_ => panic!("expected a finalized segment"),
};
assert_eq!(second.group, 1);
assert!(
matches!(live.state.read().next_after(Some(1)), Next::Pending),
"the live edge is not finalized while live"
);
live.end();
let last = match live.state.read().next_after(Some(1)) {
Next::Ready { row, .. } => row,
_ => panic!("ending finalizes the live edge"),
};
assert_eq!(last.group, 2);
assert!(matches!(live.state.read().next_after(Some(2)), Next::Ended));
}
}