Skip to main content

moq_mux/
timeline.rs

1//! Timeline publish/subscribe: the broadcast's segment index HLS/DASH export is built from.
2//!
3//! A broadcast has one timeline track, carrying one [`hang::timeline::Record`] per *segment*:
4//! a span of content time shared by every media track, mapped to the group ranges that carry
5//! it on each track. A consumer can answer "which groups cover segment N on track X" and
6//! "where is the live edge" from a few bytes per segment without subscribing to media, which
7//! is the primitive a playlist server (HLS/DASH), a seek bar, or a recorder index needs.
8//!
9//! ## Facts up, policy down
10//!
11//! The write side splits into facts, policy, and publication:
12//!
13//! - **Tracks report facts.** Each media track enrolls via [`Producer::pacing_track`], and its
14//!   [`Recorder`] reports every group open (sequence, timestamp, keyframe) plus where its
15//!   content ends. A container import never decides where segments fall; it only states what
16//!   it published.
17//! - **The timeline sets policy.** A segment ends at the first group boundary that gives it at
18//!   least [`Config::duration_min`] on every enrolled pacing track (see [`Config`] for the exact
19//!   rule). An application that knows its own boundaries overrides that with [`Producer::cut`].
20//! - **Pacing is explicit.** [`Producer::track`] safely enrolls a catalog or metadata track
21//!   without giving it control over segmentation. Continuous media opts in through
22//!   [`Producer::pacing_track`], which lets it vote on boundaries and gate completeness.
23//! - **Records close on completeness.** A segment's record is ready only once every
24//!   enrolled pacing track has reported a group at or past the segment's end (or closed), proving
25//!   the segment's group ranges are final on every track that paces. The record is then
26//!   self-contained. [`Producer::reserve`] extends that across a batch
27//!   of enrollments, the way the catalog's own reservation does.
28//! - **Publication is a commit.** [`Deferred`] yields complete records without publishing them.
29//!   A recorder can first persist the media groups they name, then pass the [`Pending`] handle to
30//!   [`Producer::push`] to make the record visible. The convenience methods on [`Producer`] commit
31//!   immediately for live publishers that need no asynchronous work between those steps.
32//!
33//! Alignment falls out of construction: every track maps its groups onto the same boundary
34//! list, so segment N covers the same span of content time on every track, which is what HLS
35//! requires of switchable renditions.
36//!
37//! ## Wiring
38//!
39//! [`catalog::Producer`](crate::catalog::Producer) owns the broadcast's timeline (the catalog
40//! is what owns the broadcast's shape) and wires all of this up:
41//! The catalog's role-specific track constructors enroll the track, which
42//! creates the timeline track on first use and advertises it in the catalog's root
43//! [`hang::catalog::Archive`] entry. A broadcast that never enrolls a track publishes no
44//! timeline at all: segmentation is opt-in per broadcast, never per track.
45//!
46//! On the read side, [`Consumer::subscribe`] reads the timeline straight from the catalog's
47//! [`hang::catalog::Archive`] entry (so the track name and timescale can't be mismatched)
48//! and yields decoded [`Event`]s. On the wire the track is a DEFLATE-compressed
49//! [`moq_json::window`], so a DVR can trim old records while an unbounded timeline simply never
50//! pops them (see [`hang::timeline`] for the record schema).
51
52use std::collections::{BTreeMap, BTreeSet, VecDeque};
53use std::ops::Deref;
54use std::sync::{Arc, Mutex};
55use std::task::Poll;
56use std::time::Duration;
57
58use hang::catalog::Archive;
59use hang::timeline::{DEFAULT_NAME, Range, Record, RecordExt};
60
61use moq_net::{Timescale, Timestamp};
62
63/// The conventional [`Config::duration_min`] (1 second), for callers with no opinion.
64pub const DEFAULT_DURATION_MIN: Duration = Duration::from_secs(1);
65
66/// Recent segment records repeated when the Window track rolls to a new group.
67const CHECKPOINT_RECORDS: usize = 256;
68
69/// How a [`Producer`] paces its segments.
70#[derive(Clone, Debug)]
71#[non_exhaustive]
72pub struct Config {
73	/// The shortest a segment may be.
74	///
75	/// A segment ends at the first point that is a group boundary on every enrolled track and
76	/// at least this far past the segment's start, so the track with the coarsest groups paces
77	/// the broadcast. A 2 second GOP against a 1 second minimum yields 2 second segments; a
78	/// real-time encoder with a 10 minute GOP yields one 10 minute segment, because there is
79	/// nowhere else a segment could start and stay decodable.
80	///
81	/// A floor rather than a target on purpose: a floor is always satisfiable (wait longer),
82	/// while a ceiling is not (a single group longer than it can't be split).
83	pub duration_min: Duration,
84
85	/// The longest a segment may be, advertised in the catalog when set.
86	///
87	/// Set it only when the publisher can actually promise one, i.e. it controls the encoder's
88	/// keyframe cadence. Consumers that need a bound up front use it (an HLS exporter's
89	/// `EXT-X-TARGETDURATION`), so it is a contract rather than a hint: a segment that would
90	/// exceed it fails the timeline instead of publishing a record that contradicts the
91	/// catalog. Leave it `None` when the media decides, which is the common case for real-time
92	/// and for anything importing a source it doesn't control.
93	pub duration_max: Option<Duration>,
94}
95
96impl Default for Config {
97	fn default() -> Self {
98		Self {
99			duration_min: DEFAULT_DURATION_MIN,
100			duration_max: None,
101		}
102	}
103}
104
105/// One enrolled track's report state.
106#[derive(Default)]
107struct TrackState {
108	/// Group opens reported and not yet flushed into a record: (sequence, pts, keyframe).
109	pending: VecDeque<(u64, Timestamp, bool)>,
110	/// The newest reported timestamp: everything earlier is known, which is what lets a
111	/// segment ending at or before it flush. Advanced by a group open (the group starts there)
112	/// and by [`Recorder::end`] (the content stops there).
113	frontier: Option<Timestamp>,
114	/// The recorder was dropped; this track no longer paces boundaries or gates completeness.
115	closed: bool,
116	/// Whether this track paces boundaries and gates completeness while open.
117	pacing: bool,
118}
119
120impl TrackState {
121	/// The first unflushed group starting at or after `threshold` micros: this track's vote for
122	/// where the open segment can end.
123	fn candidate(&self, threshold: u128) -> Option<Timestamp> {
124		self.pending
125			.iter()
126			.map(|&(_, pts, _)| pts)
127			.find(|pts| pts.as_micros() >= threshold)
128	}
129}
130
131/// The state one [`Segmenter`] guards.
132struct State {
133	config: Config,
134	/// Complete records waiting for the caller to commit.
135	ready: VecDeque<Record>,
136	/// Deferred records handed to the caller but not yet committed to the visible timeline.
137	committing: BTreeSet<u64>,
138	/// Persisted deferred records waiting for every earlier segment to be persisted too.
139	committed: BTreeMap<u64, Record>,
140	/// Where the open (unflushed) segment starts; `None` until the first report.
141	start: Option<Timestamp>,
142	/// Explicit [`Producer::cut`] boundaries not yet reached, in order.
143	cuts: VecDeque<Timestamp>,
144	/// A [`Producer::cut`] arrived, so the application owns the boundaries from here on and the
145	/// `duration_min` pacing stops. Without this the pacing races ahead of a source whose
146	/// segments are longer than the minimum, closing one before its real boundary is declared.
147	manual: bool,
148	/// The number the next flushed record gets.
149	next_segment: u64,
150	/// Every enrolled track, keyed by media track name.
151	tracks: BTreeMap<String, TrackState>,
152	/// Live [`Reserved`] handles: while any exists, no record flushes (more tracks are still
153	/// enrolling). Mirrors the catalog's own reservation gate.
154	reservers: usize,
155	/// A segment overran [`Config::duration_max`]: `(segment, duration)`. The timeline stops
156	/// publishing, since the catalog's promise is already broken.
157	overrun: Option<(u64, Duration)>,
158	/// The terminal flush ran; surviving clones and recorders can no longer mutate segmentation.
159	finished: bool,
160	/// The owning [`Producer`] stopped auto-publishing ready records.
161	deferred: bool,
162	/// The wire timescale for `pts`/`duration` (the catalog section's default: milliseconds).
163	timescale: Timescale,
164}
165
166impl State {
167	/// Return an abandoned deferred record to its segment-ordered ready queue.
168	fn requeue(&mut self, record: Record) {
169		self.committing.remove(&record.segment);
170		let index = self
171			.ready
172			.iter()
173			.position(|ready| ready.segment > record.segment)
174			.unwrap_or(self.ready.len());
175		self.ready.insert(index, record);
176	}
177
178	/// A group open reported by `name`: record the fact and close whatever became complete.
179	fn report(&mut self, name: &str, sequence: u64, pts: Timestamp, keyframe: bool) {
180		if self.finished {
181			return;
182		}
183		let Some(track) = self.tracks.get_mut(name) else {
184			return;
185		};
186		track.pending.push_back((sequence, pts, keyframe));
187		self.advance(name, pts);
188	}
189
190	/// `name`'s content ends at `pts`, without a group opening there. This is what gives the
191	/// final segment an honest duration, since its end is not a boundary anybody cut.
192	fn report_end(&mut self, name: &str, pts: Timestamp) {
193		if self.finished {
194			return;
195		}
196		if self.tracks.contains_key(name) {
197			self.advance(name, pts);
198		}
199	}
200
201	/// Raise `name`'s frontier to `pts`, then close whatever that completed.
202	fn advance(&mut self, name: &str, pts: Timestamp) {
203		if let Some(track) = self.tracks.get_mut(name)
204			&& track.frontier.is_none_or(|f| pts.as_micros() > f.as_micros())
205		{
206			track.frontier = Some(pts);
207		}
208
209		// The first thing a pacing track reports anchors the first segment, so content produced
210		// before any boundary exists belongs to the oldest segment rather than to nowhere. A
211		// non-pacing track must not anchor it: a catalog published while the encoder is still warming
212		// up would otherwise stretch segment 0 across the whole startup gap.
213		if self.start.is_none() && self.tracks.get(name).is_some_and(|t| t.pacing) {
214			self.start = Some(pts);
215		}
216
217		self.pump(false);
218	}
219
220	/// A track's recorder was dropped: stop gating completeness on it.
221	fn close(&mut self, name: &str) {
222		if self.finished {
223			return;
224		}
225		if let Some(track) = self.tracks.get_mut(name) {
226			track.closed = true;
227		}
228		self.pump(false);
229	}
230
231	/// Close every segment the media has finalized.
232	///
233	/// `finished` is the terminal pass: no track will report again, so a track that never
234	/// reached a boundary stops voting instead of holding the timeline open forever.
235	fn pump(&mut self, finished: bool) {
236		// With nothing enrolled there is nothing to describe. A record is immutable once
237		// published, so a caller that knows more tracks are still enrolling withholds it (see
238		// [`Producer::reserve`]), and an overrun has already broken the catalog's promise.
239		if self.finished || self.tracks.is_empty() || self.reservers > 0 || self.overrun.is_some() {
240			return;
241		}
242
243		while self.close_segment(finished) {
244			// A segment that broke the declared maximum ends the timeline; every later record
245			// would inherit the same broken promise.
246			if self.overrun.is_some() {
247				break;
248			}
249		}
250	}
251
252	/// Close the open segment if the media has finalized it, returning whether it did.
253	fn close_segment(&mut self, finished: bool) -> bool {
254		let Some(start) = self.start else {
255			return false;
256		};
257
258		// Discard boundaries the timeline has already reached. Only the front is ever consulted,
259		// so leaving a spent one there would block every later cut behind it and silently drop
260		// the caller back to `duration_min` pacing.
261		while self.cuts.front().is_some_and(|c| c.as_micros() <= start.as_micros()) {
262			self.cuts.pop_front();
263		}
264
265		let Some((end, cut)) = self.boundary(start, finished) else {
266			return false;
267		};
268
269		// Every open pacing track has to have reported at or past the boundary, proving its ranges
270		// for this segment are final. The track that voted for `end` has by construction; a track
271		// with shorter groups can still be behind it. A non-pacing track is excluded: it may publish
272		// rarely or never again, so waiting on it would stall the timeline for good.
273		let complete = finished
274			|| self
275				.tracks
276				.values()
277				.all(|t| !t.pacing || t.closed || t.frontier.is_some_and(|f| f.as_micros() >= end.as_micros()));
278		if !complete {
279			return false;
280		}
281
282		self.flush_segment(start, Some(end));
283		if cut {
284			self.cuts.pop_front();
285		}
286		self.start = Some(end);
287		true
288	}
289
290	/// Where the segment starting at `start` ends, once the media says so: an explicit cut when
291	/// one is registered, otherwise the first group boundary shared by every track that gives
292	/// the segment its minimum duration. The bool reports which.
293	fn boundary(&self, start: Timestamp, finished: bool) -> Option<(Timestamp, bool)> {
294		if let Some(&cut) = self.cuts.front()
295			&& cut.as_micros() > start.as_micros()
296		{
297			return Some((cut, true));
298		}
299
300		// The application declared a boundary at some point, so it owns them all: pacing here
301		// would close a segment the caller is about to cut somewhere else.
302		if self.manual {
303			return None;
304		}
305
306		let threshold = start.as_micros() + self.config.duration_min.as_micros();
307
308		let mut end: Option<Timestamp> = None;
309		for track in self.tracks.values().filter(|t| t.pacing) {
310			match track.candidate(threshold) {
311				// The latest vote wins: it is a group boundary on the coarsest track, and every
312				// finer track assigns its groups by start, so no group is split. A closed track
313				// still votes: it can't report more, but the groups it did report are boundaries
314				// like any other, and without them a backlog would collapse into one segment.
315				Some(pts) => {
316					if end.is_none_or(|e| pts.as_micros() > e.as_micros()) {
317						end = Some(pts);
318					}
319				}
320				// This track has produced nothing past the minimum yet, so ending the segment
321				// would strand it. A closed track never will, and neither does anything on the
322				// terminal pass, so neither one blocks.
323				None if finished || track.closed => continue,
324				None => return None,
325			}
326		}
327
328		end.map(|end| (end, false))
329	}
330
331	/// Build the record for the segment starting at `start`: drain every track's groups before
332	/// `end` (all of them for the final, unbounded segment) into ranges.
333	fn flush_segment(&mut self, start: Timestamp, end: Option<Timestamp>) {
334		let pts = start.as_scale(self.timescale) as u64;
335		let duration = match end {
336			Some(end) => (end.as_scale(self.timescale) as u64).saturating_sub(pts),
337			// The final segment has no end boundary, so it runs to the newest thing any track
338			// reported: its end of content when the track reported one (a finished
339			// `container::Producer` does), otherwise the last group it opened, which
340			// undercounts that group's tail.
341			None => self
342				.tracks
343				.values()
344				.filter(|t| t.pacing)
345				.filter_map(|t| t.frontier)
346				.map(|f| f.as_scale(self.timescale) as u64)
347				.max()
348				.unwrap_or(pts)
349				.saturating_sub(pts),
350		};
351
352		// The catalog promised a bound and this segment breaks it, so the record would
353		// contradict what consumers were told. Fail the timeline rather than publish it: a
354		// declared maximum the media can't honor is a bug in the publisher, and nothing a
355		// consumer reading the catalog can work around.
356		if let Some(max) = self.config.duration_max
357			&& duration > self.units(max)
358		{
359			let observed = Duration::from_nanos(duration * (1_000_000_000 / self.timescale.as_u64()));
360			tracing::error!(
361				segment = self.next_segment,
362				duration = ?observed,
363				duration_max = ?max,
364				"segment exceeded the declared duration_max; dropping the timeline track"
365			);
366			self.overrun = Some((self.next_segment, observed));
367			return;
368		}
369
370		let mut record = Record::new(self.next_segment, pts, duration);
371		self.next_segment += 1;
372
373		for (name, track) in &mut self.tracks {
374			let mut ranges: Vec<Range> = Vec::new();
375			while let Some(&(sequence, group_pts, keyframe)) = track.pending.front() {
376				// A pacing track is assigned by content time. A non-pacing track is assigned by
377				// arrival, so every group pending when the segment closes belongs to it regardless
378				// of the timestamp basis carried by that track.
379				if track.pacing && end.is_some_and(|end| group_pts.as_micros() >= end.as_micros()) {
380					break;
381				}
382				track.pending.pop_front();
383				match ranges.last_mut() {
384					// Contiguous sequences extend the run; a skip starts a new range (a gap:
385					// groups that never existed).
386					Some(last) if last.end + 1 == sequence => last.end = sequence,
387					_ => {
388						let mut range = Range::new(sequence, sequence);
389						range.keyframe = keyframe;
390						ranges.push(range);
391					}
392				}
393			}
394			if !ranges.is_empty() {
395				record.tracks.insert(name.clone(), ranges);
396			}
397		}
398
399		self.ready.push_back(record);
400	}
401
402	/// A duration in the wire timescale's units, rounded up so a bound never understates itself.
403	fn units(&self, duration: Duration) -> u64 {
404		(duration.as_micros() * self.timescale.as_u64() as u128).div_ceil(1_000_000) as u64
405	}
406
407	/// The terminal flush: close what the media finalized, then the open tail.
408	fn finish(&mut self) {
409		if self.finished {
410			return;
411		}
412
413		// Nothing more will enroll, so an outstanding reservation has nothing left to wait for.
414		self.reservers = 0;
415		self.pump(true);
416
417		// Skip an empty tail: a boundary with no content after it describes nothing.
418		if self.overrun.is_none()
419			&& let Some(start) = self.start.take()
420			&& self.tracks.values().any(|t| !t.pending.is_empty())
421		{
422			self.flush_segment(start, None);
423		}
424
425		self.finished = true;
426	}
427
428	/// The error a segment overrunning [`Config::duration_max`] left behind, if any.
429	fn failure(&self) -> Option<crate::Error> {
430		let (segment, duration) = self.overrun?;
431		Some(crate::Error::TimelineOverrun {
432			segment,
433			duration,
434			duration_max: self.config.duration_max.unwrap_or_default(),
435		})
436	}
437
438	/// The catalog section advertising this timeline.
439	///
440	/// Carries the track name, timescale, and duration bound. Wall-clock mapping is the
441	/// catalog root clock's job ([`Clock`](crate::Clock)), not this section's.
442	fn section(&self) -> Archive {
443		let mut section = Archive::new(DEFAULT_NAME);
444		section.timescale = self.timescale.as_u64() as u32;
445		section.duration_max = self.config.duration_max.map(|max| self.units(max));
446		section
447	}
448}
449
450/// Builds complete segment records without publishing them.
451///
452/// Use this directly to build records without a broadcast. An archive writer instead obtains a
453/// cancellation-safe [`Deferred`] from [`Producer::deferred`], stores the groups named by each
454/// [`Pending`] record, and passes that handle to [`Producer::push`]. `Clone` shares the
455/// segmentation state.
456#[derive(Clone)]
457pub struct Segmenter {
458	state: Arc<Mutex<State>>,
459}
460
461impl Segmenter {
462	/// Create a segmenter paced by `config`.
463	pub fn new(config: Config) -> Self {
464		// The contents are `Send + Sync` natively; on wasm moq-net's handles are
465		// `Rc`-backed, so clippy sees a pointlessly atomic `Arc`. Keeping one type for
466		// both targets is worth the unused atomics on the single-threaded one.
467		#[allow(clippy::arc_with_non_send_sync)]
468		Self {
469			state: Arc::new(Mutex::new(State {
470				config,
471				ready: VecDeque::new(),
472				committing: BTreeSet::new(),
473				committed: BTreeMap::new(),
474				start: None,
475				cuts: VecDeque::new(),
476				manual: false,
477				next_segment: 0,
478				tracks: BTreeMap::new(),
479				reservers: 0,
480				overrun: None,
481				finished: false,
482				deferred: false,
483				timescale: Timescale::MILLI,
484			})),
485		}
486	}
487
488	/// Enroll `name` without letting it influence segmentation.
489	pub fn track(&self, name: &str) -> Recorder {
490		self.enroll(name, false, None)
491	}
492
493	/// Enroll `name` as a pacing track.
494	pub fn pacing_track(&self, name: &str) -> Recorder {
495		self.enroll(name, true, None)
496	}
497
498	fn enroll(&self, name: &str, pacing: bool, output: Option<Arc<Mutex<Output>>>) -> Recorder {
499		let mut state = self.state.lock().unwrap();
500		let output = if state.finished {
501			None
502		} else {
503			state.tracks.insert(
504				name.to_string(),
505				TrackState {
506					pacing,
507					..Default::default()
508				},
509			);
510			output
511		};
512		drop(state);
513
514		Recorder {
515			segmenter: self.clone(),
516			output,
517			name: name.to_string(),
518		}
519	}
520
521	/// Declare a segment boundary at `pts`, overriding automatic minimum-duration pacing.
522	pub fn cut(&self, pts: Timestamp) -> crate::Result<()> {
523		let mut state = self.state.lock().unwrap();
524		if let Some(err) = state.failure() {
525			return Err(err);
526		}
527		if state.finished {
528			return Err(moq_net::Error::Closed.into());
529		}
530
531		state.manual = true;
532		let floor = state
533			.cuts
534			.back()
535			.copied()
536			.or(state.start)
537			.map(|since| since.as_micros() + state.config.duration_min.as_micros());
538		if floor.is_none_or(|floor| pts.as_micros() >= floor) {
539			state.cuts.push_back(pts);
540			state.pump(false);
541		}
542
543		Ok(())
544	}
545
546	/// Withhold complete records while a batch of tracks enrolls.
547	pub fn reserve(&self) -> Reserved {
548		let mut state = self.state.lock().unwrap();
549		if !state.finished {
550			state.reservers += 1;
551		}
552		Reserved {
553			segmenter: self.clone(),
554			output: None,
555		}
556	}
557
558	/// The catalog section describing records built by this segmenter.
559	pub fn section(&self) -> Archive {
560		self.state.lock().unwrap().section()
561	}
562
563	/// Flush the final open segment and return a handle that can only drain records.
564	pub fn finish(self) -> Drain {
565		let mut state = self.state.lock().unwrap();
566		state.finish();
567		drop(state);
568		Drain { segmenter: self }
569	}
570
571	/// Take the next complete segment record, if one is ready.
572	pub fn next(&self) -> Option<Record> {
573		self.state.lock().unwrap().ready.pop_front()
574	}
575}
576
577/// A finished segmenter that can only drain its final complete records.
578pub struct Drain {
579	segmenter: Segmenter,
580}
581
582impl Drain {
583	/// Report whether terminal segmentation exceeded the declared duration bound.
584	pub fn result(&self) -> crate::Result<()> {
585		result(&self.segmenter)
586	}
587
588	/// Take the next complete segment record, if one is ready.
589	pub fn next(&self) -> Option<Record> {
590		self.segmenter.next()
591	}
592}
593
594/// A deferred segmenter whose records must be persisted before publication.
595///
596/// [`next`](Self::next) returns a cancellation-safe [`Pending`] handle. Dropping the handle puts
597/// the record back at the front of the ready queue so another storage task can retry it.
598#[derive(Clone)]
599pub struct Deferred {
600	segmenter: Segmenter,
601}
602
603impl Deferred {
604	/// Enroll `name` without letting it influence segmentation.
605	pub fn track(&self, name: &str) -> Recorder {
606		self.segmenter.track(name)
607	}
608
609	/// Enroll `name` as a pacing track.
610	pub fn pacing_track(&self, name: &str) -> Recorder {
611		self.segmenter.pacing_track(name)
612	}
613
614	/// Declare a segment boundary at `pts`, overriding automatic minimum-duration pacing.
615	pub fn cut(&self, pts: Timestamp) -> crate::Result<()> {
616		self.segmenter.cut(pts)
617	}
618
619	/// Withhold complete records while a batch of tracks enrolls.
620	pub fn reserve(&self) -> Reserved {
621		self.segmenter.reserve()
622	}
623
624	/// The catalog's root `archive` entry describing records built by this segmenter.
625	pub fn section(&self) -> Archive {
626		self.segmenter.section()
627	}
628
629	/// Flush the final open segment and return a handle that can only drain pending records.
630	pub fn finish(self) -> DeferredDrain {
631		let drain = self.segmenter.finish();
632		DeferredDrain {
633			segmenter: drain.segmenter,
634		}
635	}
636
637	/// Take the next complete record for storage, if one is ready.
638	pub fn next(&self) -> Option<Pending> {
639		pending(&self.segmenter)
640	}
641}
642
643/// A finished deferred segmenter that can only drain its final records for storage.
644pub struct DeferredDrain {
645	segmenter: Segmenter,
646}
647
648impl DeferredDrain {
649	/// Report whether terminal segmentation exceeded the declared duration bound.
650	pub fn result(&self) -> crate::Result<()> {
651		result(&self.segmenter)
652	}
653
654	/// Take the next complete record for storage, if one is ready.
655	pub fn next(&self) -> Option<Pending> {
656		pending(&self.segmenter)
657	}
658}
659
660fn result(segmenter: &Segmenter) -> crate::Result<()> {
661	match segmenter.state.lock().unwrap().failure() {
662		Some(err) => Err(err),
663		None => Ok(()),
664	}
665}
666
667fn pending(segmenter: &Segmenter) -> Option<Pending> {
668	let mut state = segmenter.state.lock().unwrap();
669	let record = state.ready.pop_front()?;
670	state.committing.insert(record.segment);
671	drop(state);
672	Some(Pending {
673		segmenter: segmenter.clone(),
674		record: Some(record),
675	})
676}
677
678/// A complete deferred record being stored before timeline publication.
679///
680/// Dereference it to inspect or serialize the [`Record`]. Pass ownership to [`Producer::push`]
681/// only after storage succeeds. Dropping it requeues the record for another attempt.
682pub struct Pending {
683	segmenter: Segmenter,
684	record: Option<Record>,
685}
686
687impl Pending {
688	/// Convert a record whose media could not be stored into a gap.
689	///
690	/// Its segment number and timing stay intact, so committing it unblocks later segments without
691	/// advertising media objects that are not durable.
692	pub fn gap(mut self) -> Self {
693		self.record
694			.as_mut()
695			.expect("a pending record is present until commit")
696			.tracks
697			.clear();
698		self
699	}
700}
701
702impl Deref for Pending {
703	type Target = Record;
704
705	fn deref(&self) -> &Self::Target {
706		self.record.as_ref().expect("a pending record is present until commit")
707	}
708}
709
710impl Drop for Pending {
711	fn drop(&mut self) {
712		let Some(record) = self.record.take() else {
713			return;
714		};
715		let mut state = self.segmenter.state.lock().unwrap();
716		state.requeue(record);
717	}
718}
719
720struct Output {
721	broadcast: moq_net::broadcast::Producer,
722	sink: Option<moq_json::window::Producer<Record>>,
723	closed: bool,
724}
725
726impl Output {
727	fn prepare(&mut self) -> crate::Result<()> {
728		if self.sink.is_none() && !self.closed {
729			let info = moq_net::track::Info::default().with_priority(hang::catalog::PRIORITY.catalog);
730			let net = self.broadcast.create_track(DEFAULT_NAME, info)?;
731			let config = moq_json::window::ProducerConfig::default()
732				.with_compression(true)
733				.with_checkpoint_records(CHECKPOINT_RECORDS);
734			self.sink = Some(moq_json::window::Producer::new(net, config));
735		}
736		Ok(())
737	}
738
739	fn push(&mut self, record: &Record) -> crate::Result<()> {
740		self.prepare()?;
741		let Some(sink) = self.sink.as_mut() else {
742			return Err(moq_net::Error::Closed.into());
743		};
744		match sink.push(record) {
745			Ok(()) => Ok(()),
746			Err(moq_json::Error::Net(err)) => Err(err.into()),
747			Err(err) => Err(err.into()),
748		}
749	}
750
751	fn pop(&mut self, count: u64) -> crate::Result<()> {
752		let Some(sink) = self.sink.as_mut() else {
753			return Ok(());
754		};
755		match sink.pop(count) {
756			Ok(()) => Ok(()),
757			Err(moq_json::Error::Net(err)) => Err(err.into()),
758			Err(err) => Err(err.into()),
759		}
760	}
761
762	fn finish(&mut self) -> crate::Result<()> {
763		self.closed = true;
764		let Some(sink) = self.sink.as_mut() else {
765			return Ok(());
766		};
767		// Keep this handle alive after closing so the finished track remains discoverable for
768		// subscribers.
769		match sink.finish() {
770			Ok(()) => Ok(()),
771			Err(moq_json::Error::Net(err)) => Err(err.into()),
772			Err(err) => Err(err.into()),
773		}
774	}
775}
776
777/// The broadcast's timeline: a segmenter plus the track committed records are published on.
778///
779/// Its enrollment methods commit complete records immediately. Use [`deferred`](Self::deferred)
780/// when media must be stored before the corresponding record becomes visible.
781#[derive(Clone)]
782pub struct Producer {
783	segmenter: Segmenter,
784	output: Arc<Mutex<Output>>,
785}
786
787impl Producer {
788	/// A timeline for `broadcast`, paced by `config`.
789	pub fn new(broadcast: &moq_net::broadcast::Producer, config: Config) -> Self {
790		// Like Segmenter, the output is Send + Sync natively and Rc-backed on single-threaded wasm.
791		#[allow(clippy::arc_with_non_send_sync)]
792		let output = Arc::new(Mutex::new(Output {
793			broadcast: broadcast.clone(),
794			sink: None,
795			closed: false,
796		}));
797		Self {
798			segmenter: Segmenter::new(config),
799			output,
800		}
801	}
802
803	/// Enroll `name` without letting it influence segmentation, returning its [`Recorder`].
804	///
805	/// Its groups are recorded into whichever segment is open when they arrive, but the track
806	/// never votes on where a boundary falls and never holds a record back. This safe default is
807	/// for a catalog that emits only when renditions change, or an application's metadata track.
808	///
809	/// Placement is by arrival rather than by content time: nothing waits for the track, so a group
810	/// that arrives after its segment flushed is recorded in the next one. Frames still carry
811	/// their own timestamps. A group that never closes (a `moq_json::stream` log) is recorded once,
812	/// in the segment its group opened in.
813	///
814	/// This does not create the timeline track, so enrolling only non-pacing tracks publishes no
815	/// timeline. One recorder per track: enrolling the same name again resets its state.
816	pub fn track(&self, name: &str) -> Recorder {
817		self.segmenter.enroll(name, false, Some(self.output.clone()))
818	}
819
820	/// Enroll the media track `name` as a pacing track, returning the [`Recorder`] it reports
821	/// through.
822	///
823	/// The segment records key ranges by this name, and the track votes on boundaries and gates
824	/// completeness until its recorder drops. Enroll it when it is about to produce: an enrolled
825	/// but silent pacing track holds every record back, by design, since a segment is not complete
826	/// until every pacing track's content is known.
827	///
828	/// This opt-in is for continuously publishing media. A sparse track belongs in
829	/// [`track`](Self::track), otherwise it can stall the timeline when it goes quiet.
830	///
831	/// Creates the timeline track on first use, which errors if the broadcast cannot because
832	/// something else already took the name.
833	pub fn pacing_track(&self, name: &str) -> crate::Result<Recorder> {
834		self.output.lock().unwrap().prepare()?;
835		Ok(self.segmenter.enroll(name, true, Some(self.output.clone())))
836	}
837
838	/// Declare a segment boundary at `pts`, overriding the [`Config::duration_min`] pacing.
839	///
840	/// For applications that know their own boundaries (an HLS import following the source
841	/// playlist, CMAF segments on disk, an encoder placing keyframes). Cutting ahead of the
842	/// media is fine: the segment's record still waits for every track's groups. A cut that
843	/// would make a segment shorter than [`Config::duration_min`] is ignored, so several
844	/// producers declaring the same boundaries (the renditions of one import) cost nothing.
845	///
846	/// The first call takes over for good: [`Config::duration_min`] pacing stops, since it would
847	/// otherwise close a segment just before the caller declares where it really ends. Segments
848	/// then last exactly as long as the caller says, and the final one runs to the end of the
849	/// media.
850	///
851	/// Errors if a segment already overran [`Config::duration_max`].
852	pub fn cut(&self, pts: Timestamp) -> crate::Result<()> {
853		self.segmenter.cut(pts)?;
854		self.publish_ready();
855		Ok(())
856	}
857
858	/// Begin reserving the track set, returning a clonable [`Reserved`].
859	///
860	/// The counterpart to [`catalog::Producer::reserve`](crate::catalog::Producer::reserve), for
861	/// the same reason: while any `Reserved` clone is alive the track set may still grow, so
862	/// records are withheld from the broadcast. A record is immutable once published and its
863	/// completeness is judged against the tracks enrolled *at that moment*, so a segment that
864	/// flushes while a sibling rendition is still enrolling omits it for good, and that
865	/// rendition's earlier groups then land in whatever segment flushes next.
866	///
867	/// Hand it (or clones) to whatever brings the tracks up, so an importer that enrolls its
868	/// renditions one at a time publishes nothing until they are all in. Unlike the catalog's,
869	/// this gate is not one-shot: the catalog is a snapshot, so only its *first* publish needs
870	/// protecting, while every timeline record is an immutable log entry. Take a fresh
871	/// reservation around every batch.
872	pub fn reserve(&self) -> Reserved {
873		let mut state = self.segmenter.state.lock().unwrap();
874		let output = if state.finished {
875			None
876		} else {
877			state.reservers += 1;
878			Some(self.output.clone())
879		};
880		Reserved {
881			segmenter: self.segmenter.clone(),
882			output,
883		}
884	}
885
886	/// The catalog's root `archive` entry advertising this timeline.
887	pub fn section(&self) -> Archive {
888		self.segmenter.section()
889	}
890
891	/// Create a handle that closes segments without publishing them automatically.
892	///
893	/// Drain it with [`Deferred::next`], perform any asynchronous storage, then commit the final
894	/// record with [`push`](Self::push). Preparing the timeline track here makes that later commit
895	/// infallible with respect to track-name ownership. This switches the shared producer for good:
896	/// recorders and reservations minted before this call also stop auto-publishing.
897	pub fn deferred(&self) -> crate::Result<Deferred> {
898		// Match publish_ready's output-then-state lock order. Once this returns, no auto-publisher can
899		// still be between dequeuing a record and making it visible.
900		let mut output = self.output.lock().unwrap();
901		let mut state = self.segmenter.state.lock().unwrap();
902		if state.finished {
903			return Err(moq_net::Error::Closed.into());
904		}
905		output.prepare()?;
906		state.deferred = true;
907		Ok(Deferred {
908			segmenter: self.segmenter.clone(),
909		})
910	}
911
912	/// Reacquire the drain for a finished deferred segmenter.
913	///
914	/// This is cancellation recovery for the task that owned [`DeferredDrain`]. It is rejected
915	/// until deferred segmentation has completed.
916	pub fn drain(&self) -> crate::Result<DeferredDrain> {
917		let state = self.segmenter.state.lock().unwrap();
918		if !state.deferred || !state.finished {
919			return Err(crate::Error::TimelineDeferredPending);
920		}
921		drop(state);
922		Ok(DeferredDrain {
923			segmenter: self.segmenter.clone(),
924		})
925	}
926
927	/// Mark one complete record committed, publishing it once every earlier record is committed.
928	pub fn push(&self, mut pending: Pending) -> crate::Result<()> {
929		if !Arc::ptr_eq(&self.segmenter.state, &pending.segmenter.state) {
930			return Err(crate::Error::TimelineDeferredRecord(pending.segment));
931		}
932
933		let mut output = self.output.lock().unwrap();
934		let mut state = self.segmenter.state.lock().unwrap();
935		if !state.deferred || !state.committing.contains(&pending.segment) {
936			return Err(crate::Error::TimelineDeferredRecord(pending.segment));
937		}
938
939		let record = pending.record.take().expect("a pending record is present until commit");
940		state.committing.remove(&record.segment);
941		state.committed.insert(record.segment, record);
942
943		while let Some((&segment, record)) = state.committed.first_key_value() {
944			let earlier_ready = state.ready.front().is_some_and(|record| record.segment < segment);
945			let earlier_committing = state.committing.first().is_some_and(|pending| *pending < segment);
946			if earlier_ready || earlier_committing {
947				break;
948			}
949
950			let record = record.clone();
951			if let Err(err) = output.push(&record) {
952				let record = state.committed.remove(&segment).unwrap();
953				state.requeue(record);
954				return Err(err);
955			}
956			state.committed.remove(&segment);
957		}
958
959		Ok(())
960	}
961
962	/// Remove up to `count` oldest records from the visible timeline window.
963	pub fn pop(&self, count: u64) -> crate::Result<()> {
964		self.output.lock().unwrap().pop(count)
965	}
966
967	fn publish_ready(&self) {
968		// Serialize the dequeue and publish steps together. Recorders can report from different
969		// tasks, and taking one record before locking the output would let a later record win the
970		// output lock and become visible first.
971		let mut output = self.output.lock().unwrap();
972		if self.segmenter.state.lock().unwrap().deferred {
973			return;
974		}
975		while let Some(record) = self.segmenter.next() {
976			if let Err(err) = output.push(&record) {
977				tracing::warn!(%err, "timeline publish failed; dropping the timeline track");
978				let _ = output.finish();
979				return;
980			}
981		}
982		if self.segmenter.state.lock().unwrap().failure().is_some() {
983			let _ = output.finish();
984		}
985	}
986
987	/// Flush the final (still open) segment and finish the track.
988	///
989	/// In deferred mode, first call [`Deferred::finish`] and use its returned drain handle to store
990	/// and [`push`](Self::push) every remaining record, then call this to close the timeline track.
991	/// Calling this before the deferred segmenter finishes or while any record remains uncommitted
992	/// is rejected without changing state.
993	///
994	/// Also errors if a segment overran [`Config::duration_max`], or if the track can't be finished.
995	pub fn finish(&mut self) -> crate::Result<()> {
996		// Serialize both the mode decision and terminal flush with deferred(), which takes these
997		// locks in the same order. Either deferred mode wins before finishing starts, or finishing
998		// closes the output and a later deferred() is rejected.
999		let mut output = self.output.lock().unwrap();
1000		let mut state = self.segmenter.state.lock().unwrap();
1001		if state.deferred {
1002			if !state.finished || !state.ready.is_empty() || !state.committing.is_empty() || !state.committed.is_empty()
1003			{
1004				return Err(crate::Error::TimelineDeferredPending);
1005			}
1006			if let Some(err) = state.failure() {
1007				drop(state);
1008				let _ = output.finish();
1009				return Err(err);
1010			}
1011			drop(state);
1012			return output.finish();
1013		}
1014
1015		state.finish();
1016		if let Some(err) = state.failure() {
1017			drop(state);
1018			let _ = output.finish();
1019			return Err(err);
1020		}
1021		while let Some(record) = state.ready.pop_front() {
1022			if let Err(err) = output.push(&record) {
1023				drop(state);
1024				let _ = output.finish();
1025				return Err(err);
1026			}
1027		}
1028		drop(state);
1029		output.finish()
1030	}
1031}
1032
1033/// A clonable reservation withholding [`Producer`] records while tracks are still enrolling.
1034///
1035/// Made via [`Producer::reserve`], mirroring [`catalog::Reserved`](crate::catalog::Reserved).
1036/// Whatever became complete meanwhile flushes once the last clone drops.
1037pub struct Reserved {
1038	segmenter: Segmenter,
1039	output: Option<Arc<Mutex<Output>>>,
1040}
1041
1042impl Clone for Reserved {
1043	fn clone(&self) -> Self {
1044		let mut state = self.segmenter.state.lock().unwrap();
1045		if !state.finished {
1046			state.reservers += 1;
1047		}
1048		Self {
1049			segmenter: self.segmenter.clone(),
1050			output: self.output.clone(),
1051		}
1052	}
1053}
1054
1055impl Drop for Reserved {
1056	fn drop(&mut self) {
1057		let mut state = self.segmenter.state.lock().unwrap();
1058		state.reservers = state.reservers.saturating_sub(1);
1059		state.pump(false);
1060		drop(state);
1061		if let Some(output) = &self.output {
1062			Producer {
1063				segmenter: self.segmenter.clone(),
1064				output: output.clone(),
1065			}
1066			.publish_ready();
1067		}
1068	}
1069}
1070
1071/// Reports one media track's group opens into the shared [`Producer`].
1072///
1073/// Move-only: it is the track's single reporting handle, and dropping it closes the track's
1074/// enrollment (segments stop waiting on it). Minted by [`Producer::track`] and held by a
1075/// rendition's [`container::Producer`](crate::container::Producer).
1076pub struct Recorder {
1077	segmenter: Segmenter,
1078	output: Option<Arc<Mutex<Output>>>,
1079	name: String,
1080}
1081
1082impl Recorder {
1083	/// Report that group `sequence` opened at presentation time `pts`, `keyframe` stating
1084	/// whether its first frame is one (i.e. whether a player could join here).
1085	///
1086	/// Reports must be in group order with monotonic timestamps; this is the fact the timeline
1087	/// builds ranges, boundaries and completeness from.
1088	pub fn record(&mut self, sequence: u64, pts: Timestamp, keyframe: bool) {
1089		self.segmenter
1090			.state
1091			.lock()
1092			.unwrap()
1093			.report(&self.name, sequence, pts, keyframe);
1094		self.publish_ready();
1095	}
1096
1097	/// Report that this track's content extends to `pts`, without a group opening there.
1098	///
1099	/// A group open says where content *starts*; the last group of a broadcast has no successor
1100	/// to bound it, so its segment would otherwise be published a group short (zero for a
1101	/// segment that is a single group). Report the end whenever you know it: closing a group,
1102	/// finishing a track.
1103	pub fn end(&mut self, pts: Timestamp) {
1104		self.segmenter.state.lock().unwrap().report_end(&self.name, pts);
1105		self.publish_ready();
1106	}
1107
1108	fn publish_ready(&self) {
1109		if let Some(output) = &self.output {
1110			Producer {
1111				segmenter: self.segmenter.clone(),
1112				output: output.clone(),
1113			}
1114			.publish_ready();
1115		}
1116	}
1117}
1118
1119impl Drop for Recorder {
1120	fn drop(&mut self) {
1121		self.segmenter.state.lock().unwrap().close(&self.name);
1122		self.publish_ready();
1123	}
1124}
1125
1126/// One decoded timeline entry: a complete aligned segment with real [`Timestamp`]s and the
1127/// group ranges each track contributes.
1128#[derive(Debug, Clone, PartialEq)]
1129pub struct Entry<E: RecordExt = ()> {
1130	/// The segment's number, consecutive within the broadcast.
1131	pub segment: u64,
1132
1133	/// The segment's start.
1134	pub pts: Timestamp,
1135
1136	/// The segment's duration. The next entry starts at `pts + duration` unless content time
1137	/// itself jumped (a discontinuity).
1138	pub duration: Duration,
1139
1140	/// The group ranges each participating media track contributes, keyed by track name. A
1141	/// track absent from the map has no content in this span (HLS `EXT-X-GAP`).
1142	pub tracks: BTreeMap<String, Vec<Range>>,
1143
1144	/// The record's application extension (nothing for the default `()`).
1145	pub ext: E,
1146}
1147
1148/// One change to the visible timeline window.
1149#[derive(Debug, Clone, PartialEq)]
1150#[non_exhaustive]
1151pub enum Event<E: RecordExt = ()> {
1152	/// A complete segment record became visible at this window index.
1153	Push {
1154		/// Absolute window index assigned to the record.
1155		index: u64,
1156		/// The decoded segment entry.
1157		entry: Entry<E>,
1158	},
1159	/// These previously visible window indices were removed.
1160	Pop(std::ops::Range<u64>),
1161	/// These indices were removed before this consumer saw them.
1162	Skip(std::ops::Range<u64>),
1163}
1164
1165/// Reads a broadcast's timeline, yielding decoded [`Event`]s in window order.
1166///
1167/// Generic over the record extension `E` (see [`RecordExt`]).
1168pub struct Consumer<E: RecordExt = ()> {
1169	inner: moq_json::window::Consumer<Record<E>>,
1170	timescale: Timescale,
1171}
1172
1173impl<E: RecordExt> Consumer<E> {
1174	/// Subscribe to the timeline advertised by the catalog's [`Archive`] entry.
1175	///
1176	/// The section supplies both the track name and the timescale, so a reader can't pair the
1177	/// wrong scale with the track. Errors if the section declares a timescale that isn't
1178	/// representable.
1179	pub async fn subscribe(broadcast: &moq_net::broadcast::Consumer, section: &Archive) -> crate::Result<Self> {
1180		let track = broadcast.track(&section.track)?.subscribe(None).await?;
1181
1182		let config = moq_json::window::ConsumerConfig::default().with_compression(true);
1183
1184		Ok(Self {
1185			inner: moq_json::window::Consumer::new(track, config),
1186			timescale: Timescale::new(section.timescale as u64)
1187				.map_err(|_| crate::Error::InvalidTimescale(section.timescale))?,
1188		})
1189	}
1190
1191	/// Decode a record into an entry, converting its timing out of the wire timescale.
1192	///
1193	/// A pts the timescale can't represent is an error rather than a substituted value: silently
1194	/// moving a timestamp would misdirect seeking and live-edge logic.
1195	fn decode(&self, record: Record<E>) -> crate::Result<Entry<E>> {
1196		let scale = self.timescale.as_u64() as u128;
1197		let nanos = (record.duration as u128) * 1_000_000_000 / scale;
1198		Ok(Entry {
1199			segment: record.segment,
1200			pts: Timestamp::new(record.pts, self.timescale)?,
1201			duration: Duration::from_nanos(nanos as u64),
1202			tracks: record.tracks,
1203			ext: record.ext,
1204		})
1205	}
1206
1207	fn decode_event(&self, event: moq_json::window::Event<Record<E>>) -> crate::Result<Event<E>> {
1208		Ok(match event {
1209			moq_json::window::Event::Push { index, value } => Event::Push {
1210				index,
1211				entry: self.decode(value)?,
1212			},
1213			moq_json::window::Event::Pop(range) => Event::Pop(range),
1214			moq_json::window::Event::Skip(range) => Event::Skip(range),
1215			_ => unreachable!("unknown timeline window event"),
1216		})
1217	}
1218
1219	/// Get the next window event, or `None` once the track ends.
1220	pub async fn next(&mut self) -> crate::Result<Option<Event<E>>> {
1221		match self.inner.next().await? {
1222			Some(event) => Ok(Some(self.decode_event(event)?)),
1223			None => Ok(None),
1224		}
1225	}
1226
1227	/// Poll for the next window event, without blocking.
1228	pub fn poll_next(&mut self, waiter: &kio::Waiter) -> Poll<crate::Result<Option<Event<E>>>> {
1229		match self.inner.poll_next(waiter)? {
1230			Poll::Ready(Some(event)) => Poll::Ready(self.decode_event(event).map(Some)),
1231			Poll::Ready(None) => Poll::Ready(Ok(None)),
1232			Poll::Pending => Poll::Pending,
1233		}
1234	}
1235}
1236
1237#[cfg(test)]
1238mod test {
1239	use super::*;
1240
1241	fn ms(v: u64) -> Timestamp {
1242		Timestamp::from_millis(v).unwrap()
1243	}
1244
1245	/// Build an Entry the tests compare against.
1246	fn entry(segment: u64, pts_ms: u64, duration_ms: u64, tracks: &[(&str, &[(u64, u64)])]) -> Entry {
1247		Entry {
1248			segment,
1249			pts: ms(pts_ms),
1250			duration: Duration::from_millis(duration_ms),
1251			tracks: tracks
1252				.iter()
1253				.map(|(name, ranges)| {
1254					(
1255						name.to_string(),
1256						ranges.iter().map(|&(start, end)| Range::new(start, end)).collect(),
1257					)
1258				})
1259				.collect(),
1260			ext: (),
1261		}
1262	}
1263
1264	/// Drain a finished timeline track by subscribing to the producer's advertised section.
1265	async fn drain(broadcast: &moq_net::broadcast::Producer, producer: &Producer) -> Vec<Entry> {
1266		let mut consumer = Consumer::subscribe(&broadcast.consume(), &producer.section())
1267			.await
1268			.unwrap();
1269		drain_consumer(&mut consumer)
1270	}
1271
1272	fn drain_consumer(consumer: &mut Consumer<()>) -> Vec<Entry> {
1273		let waiter = kio::Waiter::noop();
1274		let mut out = Vec::new();
1275		while let Poll::Ready(Ok(Some(event))) = consumer.poll_next(&waiter) {
1276			if let Event::Push { entry, .. } = event {
1277				out.push(entry);
1278			}
1279		}
1280		out
1281	}
1282
1283	fn setup() -> (moq_net::broadcast::Producer, Producer) {
1284		setup_with(Config::default())
1285	}
1286
1287	fn setup_with(config: Config) -> (moq_net::broadcast::Producer, Producer) {
1288		let broadcast = moq_net::broadcast::Info::new().produce();
1289		let timeline = Producer::new(&broadcast, config);
1290		(broadcast, timeline)
1291	}
1292
1293	#[tokio::test]
1294	async fn deferred_records_are_invisible_until_committed() {
1295		let (broadcast, timeline) = setup();
1296		let segmenter = timeline.deferred().unwrap();
1297		let mut video = segmenter.pacing_track("video0");
1298		let mut consumer = Consumer::<()>::subscribe(&broadcast.consume(), &timeline.section())
1299			.await
1300			.unwrap();
1301		let waiter = kio::Waiter::noop();
1302
1303		video.record(0, ms(0), true);
1304		video.record(1, ms(2_000), true);
1305		let record = segmenter.next().expect("the complete segment is ready for storage");
1306		assert!(matches!(consumer.poll_next(&waiter), Poll::Pending));
1307
1308		timeline.push(record).unwrap();
1309		let Poll::Ready(Ok(Some(Event::Push { index, entry: actual }))) = consumer.poll_next(&waiter) else {
1310			panic!("the committed segment was not visible");
1311		};
1312		assert_eq!(index, 0);
1313		assert_eq!(actual, entry(0, 0, 2_000, &[("video0", &[(0, 0)])]));
1314
1315		timeline.pop(1).unwrap();
1316		assert!(matches!(
1317			consumer.poll_next(&waiter),
1318			Poll::Ready(Ok(Some(Event::Pop(range)))) if range == (0..1)
1319		));
1320
1321		video.end(ms(4_000));
1322		drop(video);
1323		segmenter.finish().result().unwrap();
1324	}
1325
1326	#[tokio::test]
1327	async fn deferred_disables_existing_auto_publishing_recorders() {
1328		let (broadcast, timeline) = setup();
1329		let mut video = timeline.pacing_track("video0").unwrap();
1330		let segmenter = timeline.deferred().unwrap();
1331		let mut consumer = Consumer::<()>::subscribe(&broadcast.consume(), &timeline.section())
1332			.await
1333			.unwrap();
1334		let waiter = kio::Waiter::noop();
1335
1336		video.record(0, ms(0), true);
1337		video.record(1, ms(2_000), true);
1338		let record = segmenter.next().expect("the complete segment is ready for storage");
1339		assert!(matches!(consumer.poll_next(&waiter), Poll::Pending));
1340
1341		timeline.push(record).unwrap();
1342		assert!(matches!(
1343			consumer.poll_next(&waiter),
1344			Poll::Ready(Ok(Some(Event::Push { index: 0, .. })))
1345		));
1346	}
1347
1348	#[test]
1349	fn dropping_a_pending_record_requeues_it() {
1350		let (_broadcast, timeline) = setup();
1351		let segmenter = timeline.deferred().unwrap();
1352		let mut video = segmenter.pacing_track("video0");
1353		video.record(0, ms(0), true);
1354		video.record(1, ms(2_000), true);
1355
1356		let pending = segmenter.next().expect("the complete segment is ready for storage");
1357		assert_eq!(pending.segment, 0);
1358		drop(pending);
1359
1360		let retry = segmenter.next().expect("the abandoned segment is ready to retry");
1361		assert_eq!(retry.segment, 0);
1362		timeline.push(retry).unwrap();
1363	}
1364
1365	#[test]
1366	fn ordinary_producer_rejects_a_deferred_record() {
1367		let (_ordinary_broadcast, ordinary) = setup();
1368		let (_deferred_broadcast, deferred) = setup();
1369		let segmenter = deferred.deferred().unwrap();
1370		let mut video = segmenter.pacing_track("video0");
1371		video.record(0, ms(0), true);
1372		video.record(1, ms(2_000), true);
1373
1374		let pending = segmenter.next().expect("the complete segment is ready for storage");
1375		assert!(matches!(
1376			ordinary.push(pending),
1377			Err(crate::Error::TimelineDeferredRecord(0))
1378		));
1379		assert_eq!(
1380			segmenter
1381				.next()
1382				.expect("the rejected segment returns to its producer")
1383				.segment,
1384			0
1385		);
1386	}
1387
1388	#[test]
1389	fn deferred_mode_is_rejected_after_finishing_starts() {
1390		let (_broadcast, mut timeline) = setup();
1391		timeline.finish().unwrap();
1392		assert!(matches!(
1393			timeline.deferred(),
1394			Err(crate::Error::Moq(moq_net::Error::Closed))
1395		));
1396	}
1397
1398	#[tokio::test]
1399	async fn deferred_records_publish_in_segment_order_before_finish() {
1400		let (broadcast, mut timeline) = setup();
1401		let segmenter = timeline.deferred().unwrap();
1402		let mut video = segmenter.pacing_track("video0");
1403		video.record(0, ms(0), true);
1404		video.record(1, ms(2_000), true);
1405		video.end(ms(4_000));
1406		drop(video);
1407		let mut consumer = Consumer::<()>::subscribe(&broadcast.consume(), &timeline.section())
1408			.await
1409			.unwrap();
1410		let waiter = kio::Waiter::noop();
1411
1412		assert!(matches!(timeline.finish(), Err(crate::Error::TimelineDeferredPending)));
1413		let drain = segmenter.finish();
1414		drain.result().unwrap();
1415		drop(drain);
1416		let drain = timeline.drain().expect("a cancelled terminal drain is recoverable");
1417		assert!(matches!(timeline.finish(), Err(crate::Error::TimelineDeferredPending)));
1418		// Storage failed for the first segment, so publish its timing as a gap. It must still
1419		// unblock the successfully stored segment behind it.
1420		let first = drain.next().expect("the first record is ready for storage").gap();
1421		let second = drain.next().expect("the final record is ready for storage");
1422		assert!(matches!(timeline.finish(), Err(crate::Error::TimelineDeferredPending)));
1423
1424		timeline.push(second).unwrap();
1425		assert!(matches!(consumer.poll_next(&waiter), Poll::Pending));
1426		assert!(matches!(timeline.finish(), Err(crate::Error::TimelineDeferredPending)));
1427
1428		timeline.push(first).unwrap();
1429		timeline.finish().unwrap();
1430
1431		assert_eq!(
1432			drain_consumer(&mut consumer),
1433			vec![
1434				entry(0, 0, 2_000, &[]),
1435				entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
1436			]
1437		);
1438	}
1439
1440	#[test]
1441	fn segmenter_finish_is_terminal() {
1442		let segmenter = Segmenter::new(Config::default());
1443		let late_segmenter = segmenter.clone();
1444		let mut video = segmenter.pacing_track("video0");
1445		video.record(0, ms(0), true);
1446		video.end(ms(2_000));
1447
1448		let drain = segmenter.finish();
1449		drain.result().unwrap();
1450		assert!(drain.next().is_some(), "the terminal flush keeps its final record");
1451
1452		video.record(1, ms(2_000), true);
1453		video.end(ms(4_000));
1454		let mut late = late_segmenter.pacing_track("late");
1455		late.record(0, ms(2_000), true);
1456		late.end(ms(4_000));
1457
1458		assert!(drain.next().is_none(), "surviving recorders cannot reopen the timeline");
1459		assert!(matches!(
1460			late_segmenter.cut(ms(4_000)),
1461			Err(crate::Error::Moq(moq_net::Error::Closed))
1462		));
1463	}
1464
1465	// The coarsest track paces: video GOPs are longer than duration_min, so segments are GOPs.
1466	#[tokio::test]
1467	async fn the_coarsest_track_paces_the_segments() {
1468		let (broadcast, mut timeline) = setup();
1469		let mut video = timeline.pacing_track("video0").unwrap();
1470		let mut audio = timeline.pacing_track("audio0").unwrap();
1471
1472		// Video keyframes every 2s, audio groups every 500ms, minimum 1s.
1473		video.record(0, ms(0), true);
1474		for (seq, t) in [(0u64, 0u64), (1, 500), (2, 1_000), (3, 1_500)] {
1475			audio.record(seq, ms(t), true);
1476		}
1477		video.record(1, ms(2_000), true);
1478		for (seq, t) in [(4u64, 2_000u64), (5, 2_500), (6, 3_000), (7, 3_500)] {
1479			audio.record(seq, ms(t), true);
1480		}
1481		video.record(2, ms(4_000), true);
1482		audio.record(8, ms(4_000), true);
1483		drop(video);
1484		drop(audio);
1485		timeline.finish().unwrap();
1486
1487		// Audio's own candidate (1s) loses to video's (2s), so no segment splits a GOP. Every
1488		// record is self-contained: start AND end groups, explicit duration.
1489		assert_eq!(
1490			drain(&broadcast, &timeline).await,
1491			vec![
1492				entry(0, 0, 2_000, &[("video0", &[(0, 0)]), ("audio0", &[(0, 3)])]),
1493				entry(1, 2_000, 2_000, &[("video0", &[(1, 1)]), ("audio0", &[(4, 7)])]),
1494				entry(2, 4_000, 0, &[("video0", &[(2, 2)]), ("audio0", &[(8, 8)])]),
1495			]
1496		);
1497	}
1498
1499	// A real-time encoder's GOP can dwarf the minimum. Nothing is violated: there is nowhere
1500	// else a segment could start and stay decodable, so the segment is simply long.
1501	#[tokio::test]
1502	async fn a_gop_longer_than_the_minimum_is_one_segment() {
1503		let (broadcast, mut timeline) = setup();
1504		let mut video = timeline.pacing_track("video0").unwrap();
1505
1506		video.record(0, ms(0), true);
1507		video.record(1, ms(30_000), true);
1508		video.end(ms(60_000));
1509		drop(video);
1510		timeline.finish().unwrap();
1511
1512		assert_eq!(
1513			drain(&broadcast, &timeline).await,
1514			vec![
1515				entry(0, 0, 30_000, &[("video0", &[(0, 0)])]),
1516				entry(1, 30_000, 30_000, &[("video0", &[(1, 1)])]),
1517			]
1518		);
1519	}
1520
1521	// Groups shorter than the minimum pack into one segment rather than each becoming one.
1522	#[tokio::test]
1523	async fn short_groups_pack_up_to_the_minimum() {
1524		let (broadcast, mut timeline) = setup_with(Config {
1525			duration_min: Duration::from_millis(1_500),
1526			..Default::default()
1527		});
1528		let mut audio = timeline.pacing_track("audio0").unwrap();
1529
1530		for seq in 0..8u64 {
1531			audio.record(seq, ms(seq * 500), true);
1532		}
1533		drop(audio);
1534		timeline.finish().unwrap();
1535
1536		let entries = drain(&broadcast, &timeline).await;
1537		// The first group at or past 1500ms ends segment 0, so it holds groups 0..=2.
1538		assert_eq!(entries[0], entry(0, 0, 1_500, &[("audio0", &[(0, 2)])]));
1539		assert_eq!(entries[1], entry(1, 1_500, 1_500, &[("audio0", &[(3, 5)])]));
1540	}
1541
1542	// A track that hasn't reached the minimum yet holds the segment open, so a record never
1543	// omits a rendition that was about to contribute to it.
1544	#[tokio::test]
1545	async fn a_segment_waits_for_every_track() {
1546		let (broadcast, mut timeline) = setup();
1547		let mut video = timeline.pacing_track("video0").unwrap();
1548		let mut audio = timeline.pacing_track("audio0").unwrap();
1549
1550		video.record(0, ms(0), true);
1551		audio.record(0, ms(0), true);
1552		video.record(1, ms(2_000), true);
1553		// Video alone would close segment 0 here, but audio hasn't crossed 2s.
1554		assert!(drain(&broadcast, &timeline).await.is_empty());
1555
1556		audio.record(1, ms(2_000), true);
1557		let entries = drain(&broadcast, &timeline).await;
1558		assert_eq!(
1559			entries,
1560			vec![entry(0, 0, 2_000, &[("video0", &[(0, 0)]), ("audio0", &[(0, 0)])])]
1561		);
1562		drop(video);
1563		drop(audio);
1564		timeline.finish().unwrap();
1565	}
1566
1567	// An application that knows its own boundaries overrides the pacing.
1568	#[tokio::test]
1569	async fn explicit_cuts_override_the_pacing() {
1570		let (broadcast, mut timeline) = setup();
1571		let mut video = timeline.pacing_track("video0").unwrap();
1572
1573		// Keyframes every second, cut every three: the segments follow the cuts, not the GOPs.
1574		timeline.cut(ms(3_000)).unwrap();
1575		timeline.cut(ms(6_000)).unwrap();
1576		for seq in 0..7u64 {
1577			video.record(seq, ms(seq * 1_000), true);
1578		}
1579		drop(video);
1580		timeline.finish().unwrap();
1581
1582		let entries = drain(&broadcast, &timeline).await;
1583		assert_eq!(entries[0], entry(0, 0, 3_000, &[("video0", &[(0, 2)])]));
1584		assert_eq!(entries[1], entry(1, 3_000, 3_000, &[("video0", &[(3, 5)])]));
1585	}
1586
1587	// Every rendition of one import declares the same boundaries, so a cut that would make a
1588	// segment shorter than the minimum is dropped rather than producing a stray segment.
1589	#[tokio::test]
1590	async fn a_cut_below_the_minimum_is_ignored() {
1591		let (broadcast, mut timeline) = setup();
1592		let mut video = timeline.pacing_track("video0").unwrap();
1593
1594		video.record(0, ms(0), true);
1595		timeline.cut(ms(2_000)).unwrap();
1596		// A sibling rendition's duplicate, and a boundary too close to be a segment.
1597		timeline.cut(ms(2_000)).unwrap();
1598		timeline.cut(ms(2_500)).unwrap();
1599		timeline.cut(ms(4_000)).unwrap();
1600		video.record(1, ms(2_000), true);
1601		video.record(2, ms(4_000), true);
1602		drop(video);
1603		timeline.finish().unwrap();
1604
1605		let entries = drain(&broadcast, &timeline).await;
1606		assert_eq!(entries.len(), 3, "the duplicate and the 500ms cut are both dropped");
1607		assert_eq!(entries[0], entry(0, 0, 2_000, &[("video0", &[(0, 0)])]));
1608		assert_eq!(entries[1], entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]));
1609	}
1610
1611	// The declared maximum is a contract with consumers (an HLS EXT-X-TARGETDURATION), so a
1612	// segment that breaks it fails the timeline rather than publishing a record that
1613	// contradicts the catalog.
1614	#[tokio::test]
1615	async fn exceeding_the_declared_maximum_fails_the_timeline() {
1616		let (broadcast, mut timeline) = setup_with(Config {
1617			duration_min: Duration::from_secs(1),
1618			duration_max: Some(Duration::from_secs(3)),
1619			..Default::default()
1620		});
1621		let mut video = timeline.pacing_track("video0").unwrap();
1622
1623		let mut consumer = {
1624			video.record(0, ms(0), true);
1625			video.record(1, ms(2_000), true);
1626			Consumer::<()>::subscribe(&broadcast.consume(), &timeline.section())
1627				.await
1628				.unwrap()
1629		};
1630
1631		// A 4s GOP against a declared 3s maximum.
1632		video.record(2, ms(6_000), true);
1633		drop(video);
1634
1635		let err = timeline.finish().unwrap_err();
1636		assert!(
1637			matches!(err, crate::Error::TimelineOverrun { segment: 1, .. }),
1638			"unexpected error: {err}"
1639		);
1640
1641		// The record that was still true published; the one that would have contradicted the
1642		// catalog did not, and the track ended there.
1643		let waiter = kio::Waiter::noop();
1644		let mut entries = Vec::new();
1645		while let Poll::Ready(Ok(Some(event))) = consumer.poll_next(&waiter) {
1646			if let Event::Push { entry, .. } = event {
1647				entries.push(entry);
1648			}
1649		}
1650		assert_eq!(entries, vec![entry(0, 0, 2_000, &[("video0", &[(0, 0)])])]);
1651	}
1652
1653	#[test]
1654	fn a_deferred_overrun_is_reported_when_the_output_finishes() {
1655		let (_broadcast, mut timeline) = setup_with(Config {
1656			duration_min: Duration::from_secs(1),
1657			duration_max: Some(Duration::from_secs(3)),
1658			..Default::default()
1659		});
1660		let deferred = timeline.deferred().unwrap();
1661		let mut video = deferred.pacing_track("video0");
1662
1663		video.record(0, ms(0), true);
1664		video.record(1, ms(2_000), true);
1665
1666		video.record(2, ms(6_000), true);
1667		drop(video);
1668		let drain = deferred.finish();
1669		assert!(matches!(
1670			drain.result(),
1671			Err(crate::Error::TimelineOverrun { segment: 1, .. })
1672		));
1673		// The overrun does not consume the only path to the valid prefix.
1674		timeline.push(drain.next().unwrap()).unwrap();
1675		assert!(matches!(
1676			timeline.finish(),
1677			Err(crate::Error::TimelineOverrun { segment: 1, .. })
1678		));
1679	}
1680
1681	#[tokio::test]
1682	async fn an_undeclared_maximum_is_omitted_from_the_catalog() {
1683		let (_broadcast, timeline) = setup();
1684		assert_eq!(timeline.section().duration_max, None);
1685
1686		let (_broadcast, timeline) = setup_with(Config {
1687			duration_max: Some(Duration::from_millis(2_500)),
1688			..Default::default()
1689		});
1690		assert_eq!(timeline.section().duration_max, Some(2_500));
1691	}
1692
1693	// The last group of a broadcast has no successor to bound it, so without a reported end the
1694	// final segment's duration collapses to zero (an HLS EXTINF:0).
1695	#[tokio::test]
1696	async fn the_final_segment_runs_to_the_reported_end() {
1697		let (broadcast, mut timeline) = setup();
1698		let mut video = timeline.pacing_track("video0").unwrap();
1699
1700		video.record(0, ms(0), true);
1701		video.record(1, ms(2_000), true);
1702		// A finished `container::Producer` reports where its content stops.
1703		video.end(ms(4_000));
1704		drop(video);
1705		timeline.finish().unwrap();
1706
1707		assert_eq!(
1708			drain(&broadcast, &timeline).await,
1709			vec![
1710				entry(0, 0, 2_000, &[("video0", &[(0, 0)])]),
1711				entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
1712			]
1713		);
1714	}
1715
1716	// A record is immutable and judged against the tracks enrolled when it flushes, so a
1717	// producer that enrolls its tracks one batch at a time (an HLS import walking its
1718	// renditions) holds flushing back until they are all in.
1719	#[tokio::test]
1720	async fn a_reservation_defers_flushing_until_every_track_enrolls() {
1721		let (broadcast, mut timeline) = setup();
1722		let reserved = timeline.reserve();
1723
1724		// The primary rendition runs a whole batch of segments through before its sibling has
1725		// even loaded an init segment.
1726		let mut first = timeline.pacing_track("video0").unwrap();
1727		for (seq, t) in [(0u64, 0u64), (1, 2_000), (2, 4_000)] {
1728			first.record(seq, ms(t), true);
1729		}
1730
1731		let mut second = timeline.pacing_track("video1").unwrap();
1732		for (seq, t) in [(0u64, 0u64), (1, 2_000), (2, 4_000)] {
1733			second.record(seq, ms(t), true);
1734		}
1735
1736		drop(reserved);
1737		drop(first);
1738		drop(second);
1739		timeline.finish().unwrap();
1740
1741		// Both renditions are indexed from segment 0. Without the reservation, segments 0 and 1 would
1742		// have flushed knowing only video0 (a permanent gap for video1), and video1's first two
1743		// groups would have folded into segment 2.
1744		let entries = drain(&broadcast, &timeline).await;
1745		assert_eq!(
1746			entries[0],
1747			entry(0, 0, 2_000, &[("video0", &[(0, 0)]), ("video1", &[(0, 0)])])
1748		);
1749		assert_eq!(
1750			entries[1],
1751			entry(1, 2_000, 2_000, &[("video0", &[(1, 1)]), ("video1", &[(1, 1)])])
1752		);
1753	}
1754
1755	// A track that races ahead of the others still lands in the segment its content falls in.
1756	#[tokio::test]
1757	async fn groups_before_the_first_boundary_join_the_first_segment() {
1758		let (broadcast, mut timeline) = setup();
1759		let mut video = timeline.pacing_track("video0").unwrap();
1760		let mut audio = timeline.pacing_track("audio0").unwrap();
1761
1762		// Audio races ahead of video's first keyframe (the startup race): its early group
1763		// belongs to segment 0, which starts where the earliest content does.
1764		audio.record(0, ms(0), true);
1765		video.record(0, ms(30), true);
1766		for (seq, t) in [(1u64, 500u64), (2, 1_000), (3, 1_500), (4, 2_000)] {
1767			audio.record(seq, ms(t), true);
1768		}
1769		video.record(1, ms(2_030), true);
1770		audio.record(5, ms(2_500), true);
1771		drop(video);
1772		drop(audio);
1773		timeline.finish().unwrap();
1774
1775		let entries = drain(&broadcast, &timeline).await;
1776		assert_eq!(
1777			entries[0],
1778			entry(0, 0, 2_030, &[("video0", &[(0, 0)]), ("audio0", &[(0, 4)])]),
1779		);
1780	}
1781
1782	#[tokio::test]
1783	async fn sequence_gaps_split_ranges() {
1784		let (broadcast, mut timeline) = setup();
1785		let mut video = timeline.pacing_track("video0").unwrap();
1786		let mut audio = timeline.pacing_track("audio0").unwrap();
1787
1788		// Elemental-style gap: audio groups 2..=4 never existed inside segment 0.
1789		video.record(0, ms(0), true);
1790		audio.record(0, ms(0), true);
1791		audio.record(1, ms(300), true);
1792		audio.record(5, ms(1_500), true);
1793		video.record(1, ms(2_000), true);
1794		audio.record(6, ms(2_100), true);
1795		drop(video);
1796		drop(audio);
1797		timeline.finish().unwrap();
1798
1799		let entries = drain(&broadcast, &timeline).await;
1800		assert_eq!(
1801			entries[0],
1802			entry(0, 0, 2_000, &[("video0", &[(0, 0)]), ("audio0", &[(0, 1), (5, 5)])]),
1803		);
1804	}
1805
1806	// A track with nothing to contribute is a whole-segment gap: the record simply omits it
1807	// (an HLS exporter renders EXT-X-GAP). Only a *closed* track produces one, since a live
1808	// track that has merely gone quiet still gets to say where the boundary is.
1809	#[tokio::test]
1810	async fn a_closed_track_leaves_a_whole_segment_gap() {
1811		let (broadcast, mut timeline) = setup();
1812		let mut video = timeline.pacing_track("video0").unwrap();
1813		let mut audio = timeline.pacing_track("audio0").unwrap();
1814
1815		video.record(0, ms(0), true);
1816		audio.record(0, ms(0), true);
1817		drop(audio);
1818		video.record(1, ms(2_000), true);
1819		video.record(2, ms(4_000), true);
1820		drop(video);
1821		timeline.finish().unwrap();
1822
1823		let entries = drain(&broadcast, &timeline).await;
1824		assert_eq!(entries[1], entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]));
1825	}
1826
1827	#[tokio::test]
1828	async fn a_non_keyframe_range_start_is_flagged() {
1829		let (broadcast, mut timeline) = setup();
1830		let mut video = timeline.pacing_track("video0").unwrap();
1831
1832		video.record(0, ms(0), true);
1833		// A mid-stream join: the group doesn't open on an IDR.
1834		video.record(1, ms(2_000), false);
1835		video.record(2, ms(4_000), true);
1836		drop(video);
1837		timeline.finish().unwrap();
1838
1839		let entries = drain(&broadcast, &timeline).await;
1840		let range = entries[1].tracks["video0"][0];
1841		assert!(!range.keyframe, "a range whose first group isn't an IDR says so");
1842	}
1843
1844	// A dead track would otherwise hold every later segment: dropping its recorder is what
1845	// says it will never report again.
1846	#[tokio::test]
1847	async fn a_closed_track_stops_gating() {
1848		let (broadcast, mut timeline) = setup();
1849		let mut video = timeline.pacing_track("video0").unwrap();
1850		let mut audio = timeline.pacing_track("audio0").unwrap();
1851
1852		video.record(0, ms(0), true);
1853		audio.record(0, ms(0), true);
1854		video.record(1, ms(2_000), true);
1855		assert!(drain(&broadcast, &timeline).await.is_empty(), "audio still gates");
1856
1857		drop(audio);
1858		let entries = drain(&broadcast, &timeline).await;
1859		assert_eq!(
1860			entries,
1861			vec![entry(0, 0, 2_000, &[("video0", &[(0, 0)]), ("audio0", &[(0, 0)])])]
1862		);
1863		drop(video);
1864		timeline.finish().unwrap();
1865	}
1866
1867	// The timeline track (and its catalog section) exist only once a media track enrolls.
1868	#[tokio::test]
1869	async fn the_track_is_created_on_first_enrollment() {
1870		let broadcast = moq_net::broadcast::Info::new().produce();
1871		let timeline = Producer::new(&broadcast, Config::default());
1872		assert!(
1873			broadcast.create_track(DEFAULT_NAME, None).is_ok(),
1874			"nothing took the name yet"
1875		);
1876
1877		let broadcast = moq_net::broadcast::Info::new().produce();
1878		let timeline2 = Producer::new(&broadcast, Config::default());
1879		let _recorder = timeline2.pacing_track("video0").unwrap();
1880		assert!(
1881			broadcast.create_track(DEFAULT_NAME, None).is_err(),
1882			"the timeline took the name once a track enrolled"
1883		);
1884		drop(timeline);
1885	}
1886
1887	#[tokio::test]
1888	async fn section_advertises_track_without_wall() {
1889		// Wall mapping moved to the catalog root clock: the timeline section carries the track,
1890		// timescale, and duration bound, and nothing else time-anchoring.
1891		let (_broadcast, timeline) = setup();
1892		let section = timeline.section();
1893		assert_eq!(section.track, DEFAULT_NAME);
1894		assert_eq!(section.timescale, 1000);
1895		assert_eq!(section.duration_max, None);
1896	}
1897
1898	// Clones nest like the catalog's, so the first batch to finish doesn't publish records the
1899	// others are still filling in.
1900	#[tokio::test]
1901	async fn reservations_nest() {
1902		let (broadcast, mut timeline) = setup();
1903		let mut video = timeline.pacing_track("video0").unwrap();
1904
1905		let outer = timeline.reserve();
1906		let inner = outer.clone();
1907
1908		for (seq, t) in [(0u64, 0u64), (1, 2_000), (2, 4_000)] {
1909			video.record(seq, ms(t), true);
1910		}
1911
1912		drop(inner);
1913		assert!(
1914			drain(&broadcast, &timeline).await.is_empty(),
1915			"the other clone still gates"
1916		);
1917
1918		drop(outer);
1919		assert_eq!(
1920			drain(&broadcast, &timeline).await.len(),
1921			2,
1922			"the last clone dropped flushes"
1923		);
1924
1925		drop(video);
1926		timeline.finish().unwrap();
1927	}
1928
1929	// A reservation outstanding when the broadcast ends must not strand the terminal flush.
1930	#[tokio::test]
1931	async fn finish_overrides_an_outstanding_reservation() {
1932		let (broadcast, mut timeline) = setup();
1933		let mut video = timeline.pacing_track("video0").unwrap();
1934		let reserved = timeline.reserve();
1935
1936		video.record(0, ms(0), true);
1937		video.record(1, ms(2_000), true);
1938		video.end(ms(4_000));
1939		drop(video);
1940
1941		timeline.finish().unwrap();
1942		assert_eq!(
1943			drain(&broadcast, &timeline).await,
1944			vec![
1945				entry(0, 0, 2_000, &[("video0", &[(0, 0)])]),
1946				entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
1947			]
1948		);
1949
1950		// Dropping it afterwards is a no-op rather than an underflow.
1951		drop(reserved);
1952	}
1953
1954	#[tokio::test]
1955	async fn rejects_an_invalid_timescale() {
1956		let (broadcast, timeline) = setup();
1957		let _recorder = timeline.pacing_track("video0").unwrap();
1958		let mut section = timeline.section();
1959		section.timescale = 0;
1960		let err = Consumer::<()>::subscribe(&broadcast.consume(), &section).await;
1961		assert!(matches!(err, Err(crate::Error::InvalidTimescale(0))));
1962	}
1963
1964	// A cut registered before the media, landing exactly on the first reported group (what the
1965	// fMP4 importer does: it cuts at the keyframe fragment's timestamp, then records that group).
1966	#[tokio::test]
1967	async fn a_cut_on_the_first_group_does_not_poison_later_cuts() {
1968		let (broadcast, mut timeline) = setup();
1969		let mut video = timeline.pacing_track("video0").unwrap();
1970
1971		// Source segments every 3s, keyframes every 1s: 3s segments, not the 1s the
1972		// duration_min pacing would produce on its own.
1973		timeline.cut(ms(0)).unwrap();
1974		video.record(0, ms(0), true);
1975		for seq in 1..10u64 {
1976			if seq % 3 == 0 {
1977				timeline.cut(ms(seq * 1_000)).unwrap();
1978			}
1979			video.record(seq, ms(seq * 1_000), true);
1980		}
1981		drop(video);
1982		timeline.finish().unwrap();
1983
1984		let entries = drain(&broadcast, &timeline).await;
1985		assert_eq!(
1986			entries[0],
1987			entry(0, 0, 3_000, &[("video0", &[(0, 2)])]),
1988			"the source's 3s boundaries should be reproduced, not duration_min pacing"
1989		);
1990	}
1991
1992	// A catalog publishes a group only when the renditions change, so it can go quiet for the
1993	// rest of the broadcast. Enrolled as a pacing track it would stall the timeline for good;
1994	// non-pacing, it rides along in whichever segment is open.
1995	#[tokio::test]
1996	async fn a_non_pacing_track_neither_paces_nor_gates() {
1997		let (broadcast, mut timeline) = setup();
1998		let mut video = timeline.pacing_track("video0").unwrap();
1999		let mut catalog = timeline.track("catalog.json");
2000
2001		catalog.record(0, ms(0), true);
2002		for (seq, t) in [(0u64, 0u64), (1, 2_000), (2, 4_000)] {
2003			video.record(seq, ms(t), true);
2004		}
2005		video.end(ms(6_000));
2006		timeline.finish().unwrap();
2007
2008		let entries = drain(&broadcast, &timeline).await;
2009		assert_eq!(
2010			entries,
2011			vec![
2012				entry(0, 0, 2_000, &[("catalog.json", &[(0, 0)]), ("video0", &[(0, 0)])]),
2013				entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
2014				entry(2, 4_000, 2_000, &[("video0", &[(2, 2)])]),
2015			],
2016			"the silent catalog should neither hold records back nor move a boundary"
2017		);
2018	}
2019
2020	// Nothing waits for a non-pacing track, so a group arriving after its segment already flushed
2021	// is recorded in the next one. Placement is by arrival; the frames still carry their own
2022	// timestamps.
2023	#[tokio::test]
2024	async fn a_late_non_pacing_group_lands_in_the_next_segment() {
2025		let (broadcast, mut timeline) = setup();
2026		let mut video = timeline.pacing_track("video0").unwrap();
2027		let mut catalog = timeline.track("catalog.json");
2028
2029		video.record(0, ms(0), true);
2030		// Flushes segment 0, which the catalog has contributed nothing to.
2031		video.record(1, ms(2_000), true);
2032
2033		// The update happened a second in, but only reaches the timeline now.
2034		catalog.record(0, ms(1_000), true);
2035
2036		video.record(2, ms(4_000), true);
2037		video.end(ms(6_000));
2038		timeline.finish().unwrap();
2039
2040		let entries = drain(&broadcast, &timeline).await;
2041		assert_eq!(entries[0], entry(0, 0, 2_000, &[("video0", &[(0, 0)])]));
2042		assert_eq!(
2043			entries[1],
2044			entry(1, 2_000, 2_000, &[("catalog.json", &[(0, 0)]), ("video0", &[(1, 1)])]),
2045			"the late group belongs to the segment that was open when it arrived"
2046		);
2047	}
2048
2049	// Non-pacing placement is by arrival rather than timestamp. A catalog can stamp snapshots from a
2050	// wall-clock basis while imported media starts at PTS zero, so its frontier must neither hold
2051	// the group for a much later segment nor stretch the final media segment to that timestamp.
2052	#[tokio::test]
2053	async fn a_non_pacing_track_uses_arrival_and_does_not_extend_the_tail() {
2054		let (broadcast, mut timeline) = setup();
2055		let mut video = timeline.pacing_track("video0").unwrap();
2056		let mut catalog = timeline.track("catalog.json");
2057
2058		video.record(0, ms(0), true);
2059		catalog.record(0, ms(10_000), true);
2060		video.record(1, ms(2_000), true);
2061		video.end(ms(4_000));
2062		timeline.finish().unwrap();
2063
2064		assert_eq!(
2065			drain(&broadcast, &timeline).await,
2066			vec![
2067				entry(0, 0, 2_000, &[("catalog.json", &[(0, 0)]), ("video0", &[(0, 0)])]),
2068				entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
2069			]
2070		);
2071	}
2072
2073	#[tokio::test]
2074	async fn the_built_in_catalog_is_recorded_without_pacing() {
2075		let mut broadcast = moq_net::broadcast::Info::new().produce();
2076		let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
2077		let timeline = catalog.timeline();
2078		let mut video = catalog.enroll("video0").unwrap();
2079
2080		video.record(0, ms(0), true);
2081		video.record(1, ms(2_000), true);
2082		video.end(ms(4_000));
2083		drop(video);
2084		catalog.finish().unwrap();
2085
2086		assert_eq!(
2087			drain(&broadcast, &timeline).await,
2088			vec![
2089				entry(0, 0, 2_000, &[("catalog.json", &[(0, 0)]), ("video0", &[(0, 0)])]),
2090				entry(1, 2_000, 2_000, &[("video0", &[(1, 1)])]),
2091			]
2092		);
2093	}
2094
2095	// Segmentation stays opt-in by pacing track: non-pacing tracks alone describe nothing, so the
2096	// timeline track is never created and its name stays free.
2097	#[tokio::test]
2098	async fn non_pacing_tracks_alone_publish_no_timeline() {
2099		let (broadcast, mut timeline) = setup();
2100		let mut catalog = timeline.track("catalog.json");
2101
2102		catalog.record(0, ms(0), true);
2103		timeline.finish().unwrap();
2104
2105		assert!(
2106			broadcast.create_track(DEFAULT_NAME, None).is_ok(),
2107			"the timeline track should never have been created"
2108		);
2109	}
2110}