use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::ops::Deref;
use std::sync::{Arc, Mutex};
use std::task::Poll;
use std::time::Duration;
use hang::catalog::Archive;
use hang::timeline::{DEFAULT_NAME, Range, Record, RecordExt};
use moq_net::{Timescale, Timestamp};
pub const DEFAULT_DURATION_MIN: Duration = Duration::from_secs(1);
const CHECKPOINT_RECORDS: usize = 256;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Config {
pub duration_min: Duration,
pub duration_max: Option<Duration>,
}
impl Default for Config {
fn default() -> Self {
Self {
duration_min: DEFAULT_DURATION_MIN,
duration_max: None,
}
}
}
#[derive(Default)]
struct TrackState {
pending: VecDeque<(u64, Timestamp, bool)>,
frontier: Option<Timestamp>,
closed: bool,
pacing: bool,
}
impl TrackState {
fn candidate(&self, threshold: u128) -> Option<Timestamp> {
self.pending
.iter()
.map(|&(_, pts, _)| pts)
.find(|pts| pts.as_micros() >= threshold)
}
}
struct State {
config: Config,
ready: VecDeque<Record>,
committing: BTreeSet<u64>,
committed: BTreeMap<u64, Record>,
start: Option<Timestamp>,
cuts: VecDeque<Timestamp>,
manual: bool,
next_segment: u64,
tracks: BTreeMap<String, TrackState>,
reservers: usize,
overrun: Option<(u64, Duration)>,
finished: bool,
deferred: bool,
timescale: Timescale,
}
impl State {
fn requeue(&mut self, record: Record) {
self.committing.remove(&record.segment);
let index = self
.ready
.iter()
.position(|ready| ready.segment > record.segment)
.unwrap_or(self.ready.len());
self.ready.insert(index, record);
}
fn report(&mut self, name: &str, sequence: u64, pts: Timestamp, keyframe: bool) {
if self.finished {
return;
}
let Some(track) = self.tracks.get_mut(name) else {
return;
};
track.pending.push_back((sequence, pts, keyframe));
self.advance(name, pts);
}
fn report_end(&mut self, name: &str, pts: Timestamp) {
if self.finished {
return;
}
if self.tracks.contains_key(name) {
self.advance(name, pts);
}
}
fn advance(&mut self, name: &str, pts: Timestamp) {
if let Some(track) = self.tracks.get_mut(name)
&& track.frontier.is_none_or(|f| pts.as_micros() > f.as_micros())
{
track.frontier = Some(pts);
}
if self.start.is_none() && self.tracks.get(name).is_some_and(|t| t.pacing) {
self.start = Some(pts);
}
self.pump(false);
}
fn close(&mut self, name: &str) {
if self.finished {
return;
}
if let Some(track) = self.tracks.get_mut(name) {
track.closed = true;
}
self.pump(false);
}
fn pump(&mut self, finished: bool) {
if self.finished || self.tracks.is_empty() || self.reservers > 0 || self.overrun.is_some() {
return;
}
while self.close_segment(finished) {
if self.overrun.is_some() {
break;
}
}
}
fn close_segment(&mut self, finished: bool) -> bool {
let Some(start) = self.start else {
return false;
};
while self.cuts.front().is_some_and(|c| c.as_micros() <= start.as_micros()) {
self.cuts.pop_front();
}
let Some((end, cut)) = self.boundary(start, finished) else {
return false;
};
let complete = finished
|| self
.tracks
.values()
.all(|t| !t.pacing || t.closed || t.frontier.is_some_and(|f| f.as_micros() >= end.as_micros()));
if !complete {
return false;
}
self.flush_segment(start, Some(end));
if cut {
self.cuts.pop_front();
}
self.start = Some(end);
true
}
fn boundary(&self, start: Timestamp, finished: bool) -> Option<(Timestamp, bool)> {
if let Some(&cut) = self.cuts.front()
&& cut.as_micros() > start.as_micros()
{
return Some((cut, true));
}
if self.manual {
return None;
}
let threshold = start.as_micros() + self.config.duration_min.as_micros();
let mut end: Option<Timestamp> = None;
for track in self.tracks.values().filter(|t| t.pacing) {
match track.candidate(threshold) {
Some(pts) => {
if end.is_none_or(|e| pts.as_micros() > e.as_micros()) {
end = Some(pts);
}
}
None if finished || track.closed => continue,
None => return None,
}
}
end.map(|end| (end, false))
}
fn flush_segment(&mut self, start: Timestamp, end: Option<Timestamp>) {
let pts = start.as_scale(self.timescale) as u64;
let duration = match end {
Some(end) => (end.as_scale(self.timescale) as u64).saturating_sub(pts),
None => self
.tracks
.values()
.filter(|t| t.pacing)
.filter_map(|t| t.frontier)
.map(|f| f.as_scale(self.timescale) as u64)
.max()
.unwrap_or(pts)
.saturating_sub(pts),
};
if let Some(max) = self.config.duration_max
&& duration > self.units(max)
{
let observed = Duration::from_nanos(duration * (1_000_000_000 / self.timescale.as_u64()));
tracing::error!(
segment = self.next_segment,
duration = ?observed,
duration_max = ?max,
"segment exceeded the declared duration_max; dropping the timeline track"
);
self.overrun = Some((self.next_segment, observed));
return;
}
let mut record = Record::new(self.next_segment, pts, duration);
self.next_segment += 1;
for (name, track) in &mut self.tracks {
let mut ranges: Vec<Range> = Vec::new();
while let Some(&(sequence, group_pts, keyframe)) = track.pending.front() {
if track.pacing && end.is_some_and(|end| group_pts.as_micros() >= end.as_micros()) {
break;
}
track.pending.pop_front();
match ranges.last_mut() {
Some(last) if last.end + 1 == sequence => last.end = sequence,
_ => {
let mut range = Range::new(sequence, sequence);
range.keyframe = keyframe;
ranges.push(range);
}
}
}
if !ranges.is_empty() {
record.tracks.insert(name.clone(), ranges);
}
}
self.ready.push_back(record);
}
fn units(&self, duration: Duration) -> u64 {
(duration.as_micros() * self.timescale.as_u64() as u128).div_ceil(1_000_000) as u64
}
fn finish(&mut self) {
if self.finished {
return;
}
self.reservers = 0;
self.pump(true);
if self.overrun.is_none()
&& let Some(start) = self.start.take()
&& self.tracks.values().any(|t| !t.pending.is_empty())
{
self.flush_segment(start, None);
}
self.finished = true;
}
fn failure(&self) -> Option<crate::Error> {
let (segment, duration) = self.overrun?;
Some(crate::Error::TimelineOverrun {
segment,
duration,
duration_max: self.config.duration_max.unwrap_or_default(),
})
}
fn section(&self) -> Archive {
let mut section = Archive::new(DEFAULT_NAME);
section.timescale = self.timescale.as_u64() as u32;
section.duration_max = self.config.duration_max.map(|max| self.units(max));
section
}
}
#[derive(Clone)]
pub struct Segmenter {
state: Arc<Mutex<State>>,
}
impl Segmenter {
pub fn new(config: Config) -> Self {
#[allow(clippy::arc_with_non_send_sync)]
Self {
state: Arc::new(Mutex::new(State {
config,
ready: VecDeque::new(),
committing: BTreeSet::new(),
committed: BTreeMap::new(),
start: None,
cuts: VecDeque::new(),
manual: false,
next_segment: 0,
tracks: BTreeMap::new(),
reservers: 0,
overrun: None,
finished: false,
deferred: false,
timescale: Timescale::MILLI,
})),
}
}
pub fn track(&self, name: &str) -> Recorder {
self.enroll(name, false, None)
}
pub fn pacing_track(&self, name: &str) -> Recorder {
self.enroll(name, true, None)
}
fn enroll(&self, name: &str, pacing: bool, output: Option<Arc<Mutex<Output>>>) -> Recorder {
let mut state = self.state.lock().unwrap();
let output = if state.finished {
None
} else {
state.tracks.insert(
name.to_string(),
TrackState {
pacing,
..Default::default()
},
);
output
};
drop(state);
Recorder {
segmenter: self.clone(),
output,
name: name.to_string(),
}
}
pub fn cut(&self, pts: Timestamp) -> crate::Result<()> {
let mut state = self.state.lock().unwrap();
if let Some(err) = state.failure() {
return Err(err);
}
if state.finished {
return Err(moq_net::Error::Closed.into());
}
state.manual = true;
let floor = state
.cuts
.back()
.copied()
.or(state.start)
.map(|since| since.as_micros() + state.config.duration_min.as_micros());
if floor.is_none_or(|floor| pts.as_micros() >= floor) {
state.cuts.push_back(pts);
state.pump(false);
}
Ok(())
}
pub fn reserve(&self) -> Reserved {
let mut state = self.state.lock().unwrap();
if !state.finished {
state.reservers += 1;
}
Reserved {
segmenter: self.clone(),
output: None,
}
}
pub fn section(&self) -> Archive {
self.state.lock().unwrap().section()
}
pub fn finish(self) -> Drain {
let mut state = self.state.lock().unwrap();
state.finish();
drop(state);
Drain { segmenter: self }
}
pub fn next(&self) -> Option<Record> {
self.state.lock().unwrap().ready.pop_front()
}
}
pub struct Drain {
segmenter: Segmenter,
}
impl Drain {
pub fn result(&self) -> crate::Result<()> {
result(&self.segmenter)
}
pub fn next(&self) -> Option<Record> {
self.segmenter.next()
}
}
#[derive(Clone)]
pub struct Deferred {
segmenter: Segmenter,
}
impl Deferred {
pub fn track(&self, name: &str) -> Recorder {
self.segmenter.track(name)
}
pub fn pacing_track(&self, name: &str) -> Recorder {
self.segmenter.pacing_track(name)
}
pub fn cut(&self, pts: Timestamp) -> crate::Result<()> {
self.segmenter.cut(pts)
}
pub fn reserve(&self) -> Reserved {
self.segmenter.reserve()
}
pub fn section(&self) -> Archive {
self.segmenter.section()
}
pub fn finish(self) -> DeferredDrain {
let drain = self.segmenter.finish();
DeferredDrain {
segmenter: drain.segmenter,
}
}
pub fn next(&self) -> Option<Pending> {
pending(&self.segmenter)
}
}
pub struct DeferredDrain {
segmenter: Segmenter,
}
impl DeferredDrain {
pub fn result(&self) -> crate::Result<()> {
result(&self.segmenter)
}
pub fn next(&self) -> Option<Pending> {
pending(&self.segmenter)
}
}
fn result(segmenter: &Segmenter) -> crate::Result<()> {
match segmenter.state.lock().unwrap().failure() {
Some(err) => Err(err),
None => Ok(()),
}
}
fn pending(segmenter: &Segmenter) -> Option<Pending> {
let mut state = segmenter.state.lock().unwrap();
let record = state.ready.pop_front()?;
state.committing.insert(record.segment);
drop(state);
Some(Pending {
segmenter: segmenter.clone(),
record: Some(record),
})
}
pub struct Pending {
segmenter: Segmenter,
record: Option<Record>,
}
impl Pending {
pub fn gap(mut self) -> Self {
self.record
.as_mut()
.expect("a pending record is present until commit")
.tracks
.clear();
self
}
}
impl Deref for Pending {
type Target = Record;
fn deref(&self) -> &Self::Target {
self.record.as_ref().expect("a pending record is present until commit")
}
}
impl Drop for Pending {
fn drop(&mut self) {
let Some(record) = self.record.take() else {
return;
};
let mut state = self.segmenter.state.lock().unwrap();
state.requeue(record);
}
}
struct Output {
broadcast: moq_net::broadcast::Producer,
sink: Option<moq_json::window::Producer<Record>>,
closed: bool,
}
impl Output {
fn prepare(&mut self) -> crate::Result<()> {
if self.sink.is_none() && !self.closed {
let info = moq_net::track::Info::default().with_priority(hang::catalog::PRIORITY.catalog);
let net = self.broadcast.create_track(DEFAULT_NAME, info)?;
let config = moq_json::window::ProducerConfig::default()
.with_compression(true)
.with_checkpoint_records(CHECKPOINT_RECORDS);
self.sink = Some(moq_json::window::Producer::new(net, config));
}
Ok(())
}
fn push(&mut self, record: &Record) -> crate::Result<()> {
self.prepare()?;
let Some(sink) = self.sink.as_mut() else {
return Err(moq_net::Error::Closed.into());
};
match sink.push(record) {
Ok(()) => Ok(()),
Err(moq_json::Error::Net(err)) => Err(err.into()),
Err(err) => Err(err.into()),
}
}
fn pop(&mut self, count: u64) -> crate::Result<()> {
let Some(sink) = self.sink.as_mut() else {
return Ok(());
};
match sink.pop(count) {
Ok(()) => Ok(()),
Err(moq_json::Error::Net(err)) => Err(err.into()),
Err(err) => Err(err.into()),
}
}
fn finish(&mut self) -> crate::Result<()> {
self.closed = true;
let Some(sink) = self.sink.as_mut() else {
return Ok(());
};
match sink.finish() {
Ok(()) => Ok(()),
Err(moq_json::Error::Net(err)) => Err(err.into()),
Err(err) => Err(err.into()),
}
}
}
#[derive(Clone)]
pub struct Producer {
segmenter: Segmenter,
output: Arc<Mutex<Output>>,
}
impl Producer {
pub fn new(broadcast: &moq_net::broadcast::Producer, config: Config) -> Self {
#[allow(clippy::arc_with_non_send_sync)]
let output = Arc::new(Mutex::new(Output {
broadcast: broadcast.clone(),
sink: None,
closed: false,
}));
Self {
segmenter: Segmenter::new(config),
output,
}
}
pub fn track(&self, name: &str) -> Recorder {
self.segmenter.enroll(name, false, Some(self.output.clone()))
}
pub fn pacing_track(&self, name: &str) -> crate::Result<Recorder> {
self.output.lock().unwrap().prepare()?;
Ok(self.segmenter.enroll(name, true, Some(self.output.clone())))
}
pub fn cut(&self, pts: Timestamp) -> crate::Result<()> {
self.segmenter.cut(pts)?;
self.publish_ready();
Ok(())
}
pub fn reserve(&self) -> Reserved {
let mut state = self.segmenter.state.lock().unwrap();
let output = if state.finished {
None
} else {
state.reservers += 1;
Some(self.output.clone())
};
Reserved {
segmenter: self.segmenter.clone(),
output,
}
}
pub fn section(&self) -> Archive {
self.segmenter.section()
}
pub fn deferred(&self) -> crate::Result<Deferred> {
let mut output = self.output.lock().unwrap();
let mut state = self.segmenter.state.lock().unwrap();
if state.finished {
return Err(moq_net::Error::Closed.into());
}
output.prepare()?;
state.deferred = true;
Ok(Deferred {
segmenter: self.segmenter.clone(),
})
}
pub fn drain(&self) -> crate::Result<DeferredDrain> {
let state = self.segmenter.state.lock().unwrap();
if !state.deferred || !state.finished {
return Err(crate::Error::TimelineDeferredPending);
}
drop(state);
Ok(DeferredDrain {
segmenter: self.segmenter.clone(),
})
}
pub fn push(&self, mut pending: Pending) -> crate::Result<()> {
if !Arc::ptr_eq(&self.segmenter.state, &pending.segmenter.state) {
return Err(crate::Error::TimelineDeferredRecord(pending.segment));
}
let mut output = self.output.lock().unwrap();
let mut state = self.segmenter.state.lock().unwrap();
if !state.deferred || !state.committing.contains(&pending.segment) {
return Err(crate::Error::TimelineDeferredRecord(pending.segment));
}
let record = pending.record.take().expect("a pending record is present until commit");
state.committing.remove(&record.segment);
state.committed.insert(record.segment, record);
while let Some((&segment, record)) = state.committed.first_key_value() {
let earlier_ready = state.ready.front().is_some_and(|record| record.segment < segment);
let earlier_committing = state.committing.first().is_some_and(|pending| *pending < segment);
if earlier_ready || earlier_committing {
break;
}
let record = record.clone();
if let Err(err) = output.push(&record) {
let record = state.committed.remove(&segment).unwrap();
state.requeue(record);
return Err(err);
}
state.committed.remove(&segment);
}
Ok(())
}
pub fn pop(&self, count: u64) -> crate::Result<()> {
self.output.lock().unwrap().pop(count)
}
fn publish_ready(&self) {
let mut output = self.output.lock().unwrap();
if self.segmenter.state.lock().unwrap().deferred {
return;
}
while let Some(record) = self.segmenter.next() {
if let Err(err) = output.push(&record) {
tracing::warn!(%err, "timeline publish failed; dropping the timeline track");
let _ = output.finish();
return;
}
}
if self.segmenter.state.lock().unwrap().failure().is_some() {
let _ = output.finish();
}
}
pub fn finish(&mut self) -> crate::Result<()> {
let mut output = self.output.lock().unwrap();
let mut state = self.segmenter.state.lock().unwrap();
if state.deferred {
if !state.finished || !state.ready.is_empty() || !state.committing.is_empty() || !state.committed.is_empty()
{
return Err(crate::Error::TimelineDeferredPending);
}
if let Some(err) = state.failure() {
drop(state);
let _ = output.finish();
return Err(err);
}
drop(state);
return output.finish();
}
state.finish();
if let Some(err) = state.failure() {
drop(state);
let _ = output.finish();
return Err(err);
}
while let Some(record) = state.ready.pop_front() {
if let Err(err) = output.push(&record) {
drop(state);
let _ = output.finish();
return Err(err);
}
}
drop(state);
output.finish()
}
}
pub struct Reserved {
segmenter: Segmenter,
output: Option<Arc<Mutex<Output>>>,
}
impl Clone for Reserved {
fn clone(&self) -> Self {
let mut state = self.segmenter.state.lock().unwrap();
if !state.finished {
state.reservers += 1;
}
Self {
segmenter: self.segmenter.clone(),
output: self.output.clone(),
}
}
}
impl Drop for Reserved {
fn drop(&mut self) {
let mut state = self.segmenter.state.lock().unwrap();
state.reservers = state.reservers.saturating_sub(1);
state.pump(false);
drop(state);
if let Some(output) = &self.output {
Producer {
segmenter: self.segmenter.clone(),
output: output.clone(),
}
.publish_ready();
}
}
}
pub struct Recorder {
segmenter: Segmenter,
output: Option<Arc<Mutex<Output>>>,
name: String,
}
impl Recorder {
pub fn record(&mut self, sequence: u64, pts: Timestamp, keyframe: bool) {
self.segmenter
.state
.lock()
.unwrap()
.report(&self.name, sequence, pts, keyframe);
self.publish_ready();
}
pub fn end(&mut self, pts: Timestamp) {
self.segmenter.state.lock().unwrap().report_end(&self.name, pts);
self.publish_ready();
}
fn publish_ready(&self) {
if let Some(output) = &self.output {
Producer {
segmenter: self.segmenter.clone(),
output: output.clone(),
}
.publish_ready();
}
}
}
impl Drop for Recorder {
fn drop(&mut self) {
self.segmenter.state.lock().unwrap().close(&self.name);
self.publish_ready();
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Entry<E: RecordExt = ()> {
pub segment: u64,
pub pts: Timestamp,
pub duration: Duration,
pub tracks: BTreeMap<String, Vec<Range>>,
pub ext: E,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Event<E: RecordExt = ()> {
Push {
index: u64,
entry: Entry<E>,
},
Pop(std::ops::Range<u64>),
Skip(std::ops::Range<u64>),
}
pub struct Consumer<E: RecordExt = ()> {
inner: moq_json::window::Consumer<Record<E>>,
timescale: Timescale,
}
impl<E: RecordExt> Consumer<E> {
pub async fn subscribe(broadcast: &moq_net::broadcast::Consumer, section: &Archive) -> crate::Result<Self> {
let track = broadcast.track(§ion.track)?.subscribe(None).await?;
let config = moq_json::window::ConsumerConfig::default().with_compression(true);
Ok(Self {
inner: moq_json::window::Consumer::new(track, config),
timescale: Timescale::new(section.timescale as u64)
.map_err(|_| crate::Error::InvalidTimescale(section.timescale))?,
})
}
fn decode(&self, record: Record<E>) -> crate::Result<Entry<E>> {
let scale = self.timescale.as_u64() as u128;
let nanos = (record.duration as u128) * 1_000_000_000 / scale;
Ok(Entry {
segment: record.segment,
pts: Timestamp::new(record.pts, self.timescale)?,
duration: Duration::from_nanos(nanos as u64),
tracks: record.tracks,
ext: record.ext,
})
}
fn decode_event(&self, event: moq_json::window::Event<Record<E>>) -> crate::Result<Event<E>> {
Ok(match event {
moq_json::window::Event::Push { index, value } => Event::Push {
index,
entry: self.decode(value)?,
},
moq_json::window::Event::Pop(range) => Event::Pop(range),
moq_json::window::Event::Skip(range) => Event::Skip(range),
_ => unreachable!("unknown timeline window event"),
})
}
pub async fn next(&mut self) -> crate::Result<Option<Event<E>>> {
match self.inner.next().await? {
Some(event) => Ok(Some(self.decode_event(event)?)),
None => Ok(None),
}
}
pub fn poll_next(&mut self, waiter: &kio::Waiter) -> Poll<crate::Result<Option<Event<E>>>> {
match self.inner.poll_next(waiter)? {
Poll::Ready(Some(event)) => Poll::Ready(self.decode_event(event).map(Some)),
Poll::Ready(None) => Poll::Ready(Ok(None)),
Poll::Pending => Poll::Pending,
}
}
}
#[cfg(test)]
mod test {
use super::*;
fn ms(v: u64) -> Timestamp {
Timestamp::from_millis(v).unwrap()
}
fn entry(segment: u64, pts_ms: u64, duration_ms: u64, tracks: &[(&str, &[(u64, u64)])]) -> Entry {
Entry {
segment,
pts: ms(pts_ms),
duration: Duration::from_millis(duration_ms),
tracks: tracks
.iter()
.map(|(name, ranges)| {
(
name.to_string(),
ranges.iter().map(|&(start, end)| Range::new(start, end)).collect(),
)
})
.collect(),
ext: (),
}
}
async fn drain(broadcast: &moq_net::broadcast::Producer, producer: &Producer) -> Vec<Entry> {
let mut consumer = Consumer::subscribe(&broadcast.consume(), &producer.section())
.await
.unwrap();
drain_consumer(&mut consumer)
}
fn drain_consumer(consumer: &mut Consumer<()>) -> Vec<Entry> {
let waiter = kio::Waiter::noop();
let mut out = Vec::new();
while let Poll::Ready(Ok(Some(event))) = consumer.poll_next(&waiter) {
if let Event::Push { entry, .. } = event {
out.push(entry);
}
}
out
}
fn setup() -> (moq_net::broadcast::Producer, Producer) {
setup_with(Config::default())
}
fn setup_with(config: Config) -> (moq_net::broadcast::Producer, Producer) {
let broadcast = moq_net::broadcast::Info::new().produce();
let timeline = Producer::new(&broadcast, config);
(broadcast, timeline)
}
#[tokio::test]
async fn deferred_records_are_invisible_until_committed() {
let (broadcast, timeline) = setup();
let segmenter = timeline.deferred().unwrap();
let mut video = segmenter.pacing_track("video0");
let mut consumer = Consumer::<()>::subscribe(&broadcast.consume(), &timeline.section())
.await
.unwrap();
let waiter = kio::Waiter::noop();
video.record(0, ms(0), true);
video.record(1, ms(2_000), true);
let record = segmenter.next().expect("the complete segment is ready for storage");
assert!(matches!(consumer.poll_next(&waiter), Poll::Pending));
timeline.push(record).unwrap();
let Poll::Ready(Ok(Some(Event::Push { index, entry: actual }))) = consumer.poll_next(&waiter) else {
panic!("the committed segment was not visible");
};
assert_eq!(index, 0);
assert_eq!(actual, entry(0, 0, 2_000, &[("video0", &[(0, 0)])]));
timeline.pop(1).unwrap();
assert!(matches!(
consumer.poll_next(&waiter),
Poll::Ready(Ok(Some(Event::Pop(range)))) if range == (0..1)
));
video.end(ms(4_000));
drop(video);
segmenter.finish().result().unwrap();
}
#[tokio::test]
async fn deferred_disables_existing_auto_publishing_recorders() {
let (broadcast, timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let segmenter = timeline.deferred().unwrap();
let mut consumer = Consumer::<()>::subscribe(&broadcast.consume(), &timeline.section())
.await
.unwrap();
let waiter = kio::Waiter::noop();
video.record(0, ms(0), true);
video.record(1, ms(2_000), true);
let record = segmenter.next().expect("the complete segment is ready for storage");
assert!(matches!(consumer.poll_next(&waiter), Poll::Pending));
timeline.push(record).unwrap();
assert!(matches!(
consumer.poll_next(&waiter),
Poll::Ready(Ok(Some(Event::Push { index: 0, .. })))
));
}
#[test]
fn dropping_a_pending_record_requeues_it() {
let (_broadcast, timeline) = setup();
let segmenter = timeline.deferred().unwrap();
let mut video = segmenter.pacing_track("video0");
video.record(0, ms(0), true);
video.record(1, ms(2_000), true);
let pending = segmenter.next().expect("the complete segment is ready for storage");
assert_eq!(pending.segment, 0);
drop(pending);
let retry = segmenter.next().expect("the abandoned segment is ready to retry");
assert_eq!(retry.segment, 0);
timeline.push(retry).unwrap();
}
#[test]
fn ordinary_producer_rejects_a_deferred_record() {
let (_ordinary_broadcast, ordinary) = setup();
let (_deferred_broadcast, deferred) = setup();
let segmenter = deferred.deferred().unwrap();
let mut video = segmenter.pacing_track("video0");
video.record(0, ms(0), true);
video.record(1, ms(2_000), true);
let pending = segmenter.next().expect("the complete segment is ready for storage");
assert!(matches!(
ordinary.push(pending),
Err(crate::Error::TimelineDeferredRecord(0))
));
assert_eq!(
segmenter
.next()
.expect("the rejected segment returns to its producer")
.segment,
0
);
}
#[test]
fn deferred_mode_is_rejected_after_finishing_starts() {
let (_broadcast, mut timeline) = setup();
timeline.finish().unwrap();
assert!(matches!(
timeline.deferred(),
Err(crate::Error::Moq(moq_net::Error::Closed))
));
}
#[tokio::test]
async fn deferred_records_publish_in_segment_order_before_finish() {
let (broadcast, mut timeline) = setup();
let segmenter = timeline.deferred().unwrap();
let mut video = segmenter.pacing_track("video0");
video.record(0, ms(0), true);
video.record(1, ms(2_000), true);
video.end(ms(4_000));
drop(video);
let mut consumer = Consumer::<()>::subscribe(&broadcast.consume(), &timeline.section())
.await
.unwrap();
let waiter = kio::Waiter::noop();
assert!(matches!(timeline.finish(), Err(crate::Error::TimelineDeferredPending)));
let drain = segmenter.finish();
drain.result().unwrap();
drop(drain);
let drain = timeline.drain().expect("a cancelled terminal drain is recoverable");
assert!(matches!(timeline.finish(), Err(crate::Error::TimelineDeferredPending)));
let first = drain.next().expect("the first record is ready for storage").gap();
let second = drain.next().expect("the final record is ready for storage");
assert!(matches!(timeline.finish(), Err(crate::Error::TimelineDeferredPending)));
timeline.push(second).unwrap();
assert!(matches!(consumer.poll_next(&waiter), Poll::Pending));
assert!(matches!(timeline.finish(), Err(crate::Error::TimelineDeferredPending)));
timeline.push(first).unwrap();
timeline.finish().unwrap();
assert_eq!(
drain_consumer(&mut consumer),
vec![
entry(0, 0, 2_000, &[]),
entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
]
);
}
#[test]
fn segmenter_finish_is_terminal() {
let segmenter = Segmenter::new(Config::default());
let late_segmenter = segmenter.clone();
let mut video = segmenter.pacing_track("video0");
video.record(0, ms(0), true);
video.end(ms(2_000));
let drain = segmenter.finish();
drain.result().unwrap();
assert!(drain.next().is_some(), "the terminal flush keeps its final record");
video.record(1, ms(2_000), true);
video.end(ms(4_000));
let mut late = late_segmenter.pacing_track("late");
late.record(0, ms(2_000), true);
late.end(ms(4_000));
assert!(drain.next().is_none(), "surviving recorders cannot reopen the timeline");
assert!(matches!(
late_segmenter.cut(ms(4_000)),
Err(crate::Error::Moq(moq_net::Error::Closed))
));
}
#[tokio::test]
async fn the_coarsest_track_paces_the_segments() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let mut audio = timeline.pacing_track("audio0").unwrap();
video.record(0, ms(0), true);
for (seq, t) in [(0u64, 0u64), (1, 500), (2, 1_000), (3, 1_500)] {
audio.record(seq, ms(t), true);
}
video.record(1, ms(2_000), true);
for (seq, t) in [(4u64, 2_000u64), (5, 2_500), (6, 3_000), (7, 3_500)] {
audio.record(seq, ms(t), true);
}
video.record(2, ms(4_000), true);
audio.record(8, ms(4_000), true);
drop(video);
drop(audio);
timeline.finish().unwrap();
assert_eq!(
drain(&broadcast, &timeline).await,
vec![
entry(0, 0, 2_000, &[("video0", &[(0, 0)]), ("audio0", &[(0, 3)])]),
entry(1, 2_000, 2_000, &[("video0", &[(1, 1)]), ("audio0", &[(4, 7)])]),
entry(2, 4_000, 0, &[("video0", &[(2, 2)]), ("audio0", &[(8, 8)])]),
]
);
}
#[tokio::test]
async fn a_gop_longer_than_the_minimum_is_one_segment() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
video.record(0, ms(0), true);
video.record(1, ms(30_000), true);
video.end(ms(60_000));
drop(video);
timeline.finish().unwrap();
assert_eq!(
drain(&broadcast, &timeline).await,
vec![
entry(0, 0, 30_000, &[("video0", &[(0, 0)])]),
entry(1, 30_000, 30_000, &[("video0", &[(1, 1)])]),
]
);
}
#[tokio::test]
async fn short_groups_pack_up_to_the_minimum() {
let (broadcast, mut timeline) = setup_with(Config {
duration_min: Duration::from_millis(1_500),
..Default::default()
});
let mut audio = timeline.pacing_track("audio0").unwrap();
for seq in 0..8u64 {
audio.record(seq, ms(seq * 500), true);
}
drop(audio);
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
assert_eq!(entries[0], entry(0, 0, 1_500, &[("audio0", &[(0, 2)])]));
assert_eq!(entries[1], entry(1, 1_500, 1_500, &[("audio0", &[(3, 5)])]));
}
#[tokio::test]
async fn a_segment_waits_for_every_track() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let mut audio = timeline.pacing_track("audio0").unwrap();
video.record(0, ms(0), true);
audio.record(0, ms(0), true);
video.record(1, ms(2_000), true);
assert!(drain(&broadcast, &timeline).await.is_empty());
audio.record(1, ms(2_000), true);
let entries = drain(&broadcast, &timeline).await;
assert_eq!(
entries,
vec![entry(0, 0, 2_000, &[("video0", &[(0, 0)]), ("audio0", &[(0, 0)])])]
);
drop(video);
drop(audio);
timeline.finish().unwrap();
}
#[tokio::test]
async fn explicit_cuts_override_the_pacing() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
timeline.cut(ms(3_000)).unwrap();
timeline.cut(ms(6_000)).unwrap();
for seq in 0..7u64 {
video.record(seq, ms(seq * 1_000), true);
}
drop(video);
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
assert_eq!(entries[0], entry(0, 0, 3_000, &[("video0", &[(0, 2)])]));
assert_eq!(entries[1], entry(1, 3_000, 3_000, &[("video0", &[(3, 5)])]));
}
#[tokio::test]
async fn a_cut_below_the_minimum_is_ignored() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
video.record(0, ms(0), true);
timeline.cut(ms(2_000)).unwrap();
timeline.cut(ms(2_000)).unwrap();
timeline.cut(ms(2_500)).unwrap();
timeline.cut(ms(4_000)).unwrap();
video.record(1, ms(2_000), true);
video.record(2, ms(4_000), true);
drop(video);
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
assert_eq!(entries.len(), 3, "the duplicate and the 500ms cut are both dropped");
assert_eq!(entries[0], entry(0, 0, 2_000, &[("video0", &[(0, 0)])]));
assert_eq!(entries[1], entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]));
}
#[tokio::test]
async fn exceeding_the_declared_maximum_fails_the_timeline() {
let (broadcast, mut timeline) = setup_with(Config {
duration_min: Duration::from_secs(1),
duration_max: Some(Duration::from_secs(3)),
..Default::default()
});
let mut video = timeline.pacing_track("video0").unwrap();
let mut consumer = {
video.record(0, ms(0), true);
video.record(1, ms(2_000), true);
Consumer::<()>::subscribe(&broadcast.consume(), &timeline.section())
.await
.unwrap()
};
video.record(2, ms(6_000), true);
drop(video);
let err = timeline.finish().unwrap_err();
assert!(
matches!(err, crate::Error::TimelineOverrun { segment: 1, .. }),
"unexpected error: {err}"
);
let waiter = kio::Waiter::noop();
let mut entries = Vec::new();
while let Poll::Ready(Ok(Some(event))) = consumer.poll_next(&waiter) {
if let Event::Push { entry, .. } = event {
entries.push(entry);
}
}
assert_eq!(entries, vec![entry(0, 0, 2_000, &[("video0", &[(0, 0)])])]);
}
#[test]
fn a_deferred_overrun_is_reported_when_the_output_finishes() {
let (_broadcast, mut timeline) = setup_with(Config {
duration_min: Duration::from_secs(1),
duration_max: Some(Duration::from_secs(3)),
..Default::default()
});
let deferred = timeline.deferred().unwrap();
let mut video = deferred.pacing_track("video0");
video.record(0, ms(0), true);
video.record(1, ms(2_000), true);
video.record(2, ms(6_000), true);
drop(video);
let drain = deferred.finish();
assert!(matches!(
drain.result(),
Err(crate::Error::TimelineOverrun { segment: 1, .. })
));
timeline.push(drain.next().unwrap()).unwrap();
assert!(matches!(
timeline.finish(),
Err(crate::Error::TimelineOverrun { segment: 1, .. })
));
}
#[tokio::test]
async fn an_undeclared_maximum_is_omitted_from_the_catalog() {
let (_broadcast, timeline) = setup();
assert_eq!(timeline.section().duration_max, None);
let (_broadcast, timeline) = setup_with(Config {
duration_max: Some(Duration::from_millis(2_500)),
..Default::default()
});
assert_eq!(timeline.section().duration_max, Some(2_500));
}
#[tokio::test]
async fn the_final_segment_runs_to_the_reported_end() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
video.record(0, ms(0), true);
video.record(1, ms(2_000), true);
video.end(ms(4_000));
drop(video);
timeline.finish().unwrap();
assert_eq!(
drain(&broadcast, &timeline).await,
vec![
entry(0, 0, 2_000, &[("video0", &[(0, 0)])]),
entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
]
);
}
#[tokio::test]
async fn a_reservation_defers_flushing_until_every_track_enrolls() {
let (broadcast, mut timeline) = setup();
let reserved = timeline.reserve();
let mut first = timeline.pacing_track("video0").unwrap();
for (seq, t) in [(0u64, 0u64), (1, 2_000), (2, 4_000)] {
first.record(seq, ms(t), true);
}
let mut second = timeline.pacing_track("video1").unwrap();
for (seq, t) in [(0u64, 0u64), (1, 2_000), (2, 4_000)] {
second.record(seq, ms(t), true);
}
drop(reserved);
drop(first);
drop(second);
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
assert_eq!(
entries[0],
entry(0, 0, 2_000, &[("video0", &[(0, 0)]), ("video1", &[(0, 0)])])
);
assert_eq!(
entries[1],
entry(1, 2_000, 2_000, &[("video0", &[(1, 1)]), ("video1", &[(1, 1)])])
);
}
#[tokio::test]
async fn groups_before_the_first_boundary_join_the_first_segment() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let mut audio = timeline.pacing_track("audio0").unwrap();
audio.record(0, ms(0), true);
video.record(0, ms(30), true);
for (seq, t) in [(1u64, 500u64), (2, 1_000), (3, 1_500), (4, 2_000)] {
audio.record(seq, ms(t), true);
}
video.record(1, ms(2_030), true);
audio.record(5, ms(2_500), true);
drop(video);
drop(audio);
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
assert_eq!(
entries[0],
entry(0, 0, 2_030, &[("video0", &[(0, 0)]), ("audio0", &[(0, 4)])]),
);
}
#[tokio::test]
async fn sequence_gaps_split_ranges() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let mut audio = timeline.pacing_track("audio0").unwrap();
video.record(0, ms(0), true);
audio.record(0, ms(0), true);
audio.record(1, ms(300), true);
audio.record(5, ms(1_500), true);
video.record(1, ms(2_000), true);
audio.record(6, ms(2_100), true);
drop(video);
drop(audio);
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
assert_eq!(
entries[0],
entry(0, 0, 2_000, &[("video0", &[(0, 0)]), ("audio0", &[(0, 1), (5, 5)])]),
);
}
#[tokio::test]
async fn a_closed_track_leaves_a_whole_segment_gap() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let mut audio = timeline.pacing_track("audio0").unwrap();
video.record(0, ms(0), true);
audio.record(0, ms(0), true);
drop(audio);
video.record(1, ms(2_000), true);
video.record(2, ms(4_000), true);
drop(video);
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
assert_eq!(entries[1], entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]));
}
#[tokio::test]
async fn a_non_keyframe_range_start_is_flagged() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
video.record(0, ms(0), true);
video.record(1, ms(2_000), false);
video.record(2, ms(4_000), true);
drop(video);
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
let range = entries[1].tracks["video0"][0];
assert!(!range.keyframe, "a range whose first group isn't an IDR says so");
}
#[tokio::test]
async fn a_closed_track_stops_gating() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let mut audio = timeline.pacing_track("audio0").unwrap();
video.record(0, ms(0), true);
audio.record(0, ms(0), true);
video.record(1, ms(2_000), true);
assert!(drain(&broadcast, &timeline).await.is_empty(), "audio still gates");
drop(audio);
let entries = drain(&broadcast, &timeline).await;
assert_eq!(
entries,
vec![entry(0, 0, 2_000, &[("video0", &[(0, 0)]), ("audio0", &[(0, 0)])])]
);
drop(video);
timeline.finish().unwrap();
}
#[tokio::test]
async fn the_track_is_created_on_first_enrollment() {
let broadcast = moq_net::broadcast::Info::new().produce();
let timeline = Producer::new(&broadcast, Config::default());
assert!(
broadcast.create_track(DEFAULT_NAME, None).is_ok(),
"nothing took the name yet"
);
let broadcast = moq_net::broadcast::Info::new().produce();
let timeline2 = Producer::new(&broadcast, Config::default());
let _recorder = timeline2.pacing_track("video0").unwrap();
assert!(
broadcast.create_track(DEFAULT_NAME, None).is_err(),
"the timeline took the name once a track enrolled"
);
drop(timeline);
}
#[tokio::test]
async fn section_advertises_track_without_wall() {
let (_broadcast, timeline) = setup();
let section = timeline.section();
assert_eq!(section.track, DEFAULT_NAME);
assert_eq!(section.timescale, 1000);
assert_eq!(section.duration_max, None);
}
#[tokio::test]
async fn reservations_nest() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let outer = timeline.reserve();
let inner = outer.clone();
for (seq, t) in [(0u64, 0u64), (1, 2_000), (2, 4_000)] {
video.record(seq, ms(t), true);
}
drop(inner);
assert!(
drain(&broadcast, &timeline).await.is_empty(),
"the other clone still gates"
);
drop(outer);
assert_eq!(
drain(&broadcast, &timeline).await.len(),
2,
"the last clone dropped flushes"
);
drop(video);
timeline.finish().unwrap();
}
#[tokio::test]
async fn finish_overrides_an_outstanding_reservation() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let reserved = timeline.reserve();
video.record(0, ms(0), true);
video.record(1, ms(2_000), true);
video.end(ms(4_000));
drop(video);
timeline.finish().unwrap();
assert_eq!(
drain(&broadcast, &timeline).await,
vec![
entry(0, 0, 2_000, &[("video0", &[(0, 0)])]),
entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
]
);
drop(reserved);
}
#[tokio::test]
async fn rejects_an_invalid_timescale() {
let (broadcast, timeline) = setup();
let _recorder = timeline.pacing_track("video0").unwrap();
let mut section = timeline.section();
section.timescale = 0;
let err = Consumer::<()>::subscribe(&broadcast.consume(), §ion).await;
assert!(matches!(err, Err(crate::Error::InvalidTimescale(0))));
}
#[tokio::test]
async fn a_cut_on_the_first_group_does_not_poison_later_cuts() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
timeline.cut(ms(0)).unwrap();
video.record(0, ms(0), true);
for seq in 1..10u64 {
if seq % 3 == 0 {
timeline.cut(ms(seq * 1_000)).unwrap();
}
video.record(seq, ms(seq * 1_000), true);
}
drop(video);
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
assert_eq!(
entries[0],
entry(0, 0, 3_000, &[("video0", &[(0, 2)])]),
"the source's 3s boundaries should be reproduced, not duration_min pacing"
);
}
#[tokio::test]
async fn a_non_pacing_track_neither_paces_nor_gates() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let mut catalog = timeline.track("catalog.json");
catalog.record(0, ms(0), true);
for (seq, t) in [(0u64, 0u64), (1, 2_000), (2, 4_000)] {
video.record(seq, ms(t), true);
}
video.end(ms(6_000));
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
assert_eq!(
entries,
vec![
entry(0, 0, 2_000, &[("catalog.json", &[(0, 0)]), ("video0", &[(0, 0)])]),
entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
entry(2, 4_000, 2_000, &[("video0", &[(2, 2)])]),
],
"the silent catalog should neither hold records back nor move a boundary"
);
}
#[tokio::test]
async fn a_late_non_pacing_group_lands_in_the_next_segment() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let mut catalog = timeline.track("catalog.json");
video.record(0, ms(0), true);
video.record(1, ms(2_000), true);
catalog.record(0, ms(1_000), true);
video.record(2, ms(4_000), true);
video.end(ms(6_000));
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
assert_eq!(entries[0], entry(0, 0, 2_000, &[("video0", &[(0, 0)])]));
assert_eq!(
entries[1],
entry(1, 2_000, 2_000, &[("catalog.json", &[(0, 0)]), ("video0", &[(1, 1)])]),
"the late group belongs to the segment that was open when it arrived"
);
}
#[tokio::test]
async fn a_non_pacing_track_uses_arrival_and_does_not_extend_the_tail() {
let (broadcast, mut timeline) = setup();
let mut video = timeline.pacing_track("video0").unwrap();
let mut catalog = timeline.track("catalog.json");
video.record(0, ms(0), true);
catalog.record(0, ms(10_000), true);
video.record(1, ms(2_000), true);
video.end(ms(4_000));
timeline.finish().unwrap();
assert_eq!(
drain(&broadcast, &timeline).await,
vec![
entry(0, 0, 2_000, &[("catalog.json", &[(0, 0)]), ("video0", &[(0, 0)])]),
entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
]
);
}
#[tokio::test]
async fn the_built_in_catalog_is_recorded_without_pacing() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let timeline = catalog.timeline();
let mut video = catalog.enroll("video0").unwrap();
video.record(0, ms(0), true);
video.record(1, ms(2_000), true);
video.end(ms(4_000));
drop(video);
catalog.finish().unwrap();
assert_eq!(
drain(&broadcast, &timeline).await,
vec![
entry(0, 0, 2_000, &[("catalog.json", &[(0, 0)]), ("video0", &[(0, 0)])]),
entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
]
);
}
#[tokio::test]
async fn non_pacing_tracks_alone_publish_no_timeline() {
let (broadcast, mut timeline) = setup();
let mut catalog = timeline.track("catalog.json");
catalog.record(0, ms(0), true);
timeline.finish().unwrap();
assert!(
broadcast.create_track(DEFAULT_NAME, None).is_ok(),
"the timeline track should never have been created"
);
}
}