Skip to main content

moq_mux/container/
consumer.rs

1use std::collections::VecDeque;
2use std::task::{Poll, ready};
3
4use moq_net::Timestamp;
5
6use super::{Container, Frame};
7
8/// Decode a moq-lite track into a stream of media [`Frame`]s in latency-bounded
9/// presentation order.
10///
11/// `Consumer` wraps a [`moq_net::track::Subscriber`] and a [`Container`]
12/// format implementation, typically
13/// [`catalog::hang::Container`](crate::catalog::hang::Container). Yields
14/// decoded frames via [`read`](Self::read).
15///
16/// ## Ordering & latency skipping
17///
18/// Groups can arrive on the wire out of order. The consumer always reads frames *within*
19/// a group in arrival order, but across groups it advances by sequence number, skipping
20/// stalled or missing groups when the difference between the oldest pending timestamp
21/// and the newest available timestamp exceeds the configured latency. With the default
22/// latency of zero, the consumer skips aggressively. Any group that has a newer
23/// alternative is dropped. With a non-zero latency, slow groups are tolerated up to that
24/// budget before being skipped.
25///
26/// A stalled group is also skipped early, regardless of the latency budget, once it has
27/// presented up to where the next group begins. CMAF frames carry a per-sample duration,
28/// so a group whose most recent frame ends (timestamp + duration) at or past the next
29/// group's first timestamp has nothing left worth waiting for. Containers without a
30/// duration report zero, which disables this check and falls back to the latency budget.
31///
32/// Set the latency with [`with_latency`](Self::with_latency) (builder) or
33/// [`set_latency`](Self::set_latency) (mid-stream).
34///
35/// ## Timeline rewinds
36///
37/// If a newer group's timestamps jump backwards past the live edge, the publisher is
38/// reneging the buffered tail (e.g. a voice agent interrupted mid-utterance). The consumer
39/// drops the reneged groups, resumes at the rewound timeline, and bumps
40/// [`discontinuity`](Self::discontinuity) so downstream consumers can flush their own
41/// buffers. This is always on.
42pub struct Consumer<F: Container> {
43	track: moq_net::track::Subscriber,
44
45	format: F,
46
47	// The current group that we want to read from
48	current: u64,
49
50	// Groups that we are monitoring, sorted by sequence ascending.
51	pending: VecDeque<GroupBuffer>,
52
53	// When true, we haven't returned a frame yet and need to select the first group.
54	// We wait until we have at least one frame before finalizing `current`
55	startup: bool,
56
57	// The maximum buffer size before skipping a group.
58	latency: std::time::Duration,
59
60	// Timeline-rewind tracking: the live edge, the active boundary, and the discontinuity count.
61	rewind: Rewind,
62}
63
64/// Live state for detecting timeline rewinds and classifying out-of-order groups.
65///
66/// A publisher reneges its buffered tail by rewinding timestamps while group sequence keeps
67/// climbing (e.g. a voice agent interrupted mid-utterance). We track the live edge to spot the
68/// jump, a [`Reset`] boundary to classify out-of-order groups across it, and a counter that
69/// downstream consumers watch to flush their own queues.
70#[derive(Default)]
71struct Rewind {
72	// The live edge of playback: the largest timestamp delivered so far and the group that
73	// carried it. `None` until the first frame is delivered.
74	live_edge: Option<(u64, Timestamp)>,
75
76	// The active rewind boundary, if any. Out-of-order groups are classified against it so a
77	// late new-epoch group is kept while a reneged old-epoch straggler is dropped.
78	boundary: Option<Reset>,
79
80	// Increments on every rewind. Downstream consumers compare it across reads and, when it
81	// changes, drop media still queued in their decoder or render buffers.
82	discontinuity: u64,
83}
84
85/// A recorded rewind boundary.
86///
87/// After a backwards timestamp jump, groups can still arrive out of order, so a single
88/// sequence floor is not enough: a late new-epoch group can have a *lower* sequence than
89/// the group that triggered detection. We keep just enough state to classify any group by
90/// `(sequence, timestamp)`.
91#[derive(Clone, Copy)]
92struct Reset {
93	// Highest-sequence old-epoch group seen at detection (it held the old live edge).
94	// Sequences at or below this are old: drop.
95	prev_max: u64,
96
97	// The group whose backwards timestamp triggered detection. Sequences at or above this
98	// are new: keep.
99	group: u64,
100
101	// That group's timestamp. Within the ambiguous span `(prev_max, group)` a group is a
102	// new-epoch gap-filler if its timestamp is below this, else an old straggler whose
103	// higher timestamp simply hadn't arrived yet.
104	timestamp: Timestamp,
105}
106
107impl Reset {
108	// Classify by sequence alone. `Some(true)` = old/drop, `Some(false)` = new/keep,
109	// `None` = ambiguous (the caller must resolve it with the group's timestamp).
110	fn by_sequence(&self, sequence: u64) -> Option<bool> {
111		if sequence <= self.prev_max {
112			Some(true)
113		} else if sequence >= self.group {
114			Some(false)
115		} else {
116			None
117		}
118	}
119
120	// Whether a group belongs to the reneged old epoch and should be dropped. In the
121	// ambiguous span, old stragglers sit at or above the reset timestamp; new gap-fillers
122	// fall below it.
123	fn is_stale(&self, sequence: u64, timestamp: Timestamp) -> bool {
124		self.by_sequence(sequence).unwrap_or(timestamp >= self.timestamp)
125	}
126}
127
128impl<F: Container> Consumer<F> {
129	/// Create a Consumer wrapping the given moq-lite consumer, decoding `format`.
130	///
131	/// Skips aggressively by default; raise the tolerance with [`with_latency`](Self::with_latency).
132	pub fn new(track: moq_net::track::Subscriber, format: F) -> Self {
133		Self {
134			track,
135			format,
136			current: 0,
137			pending: VecDeque::new(),
138			startup: true,
139			latency: std::time::Duration::ZERO,
140			rewind: Rewind::default(),
141		}
142	}
143
144	/// Set the maximum latency tolerance.
145	///
146	/// Groups with timestamps older than the newest timestamp minus this value are skipped. Zero
147	/// (the default) skips aggressively: any group with a newer alternative is dropped.
148	pub fn with_latency(mut self, latency: std::time::Duration) -> Self {
149		self.latency = latency;
150		self
151	}
152
153	/// A counter that increments each time the consumer detects a timeline rewind and drops
154	/// the reneged buffer.
155	///
156	/// When a newer group's timestamps jump backwards past the live edge, the publisher is
157	/// reneging everything buffered after that point (e.g. a voice agent interrupted
158	/// mid-utterance). Downstream consumers should compare this across reads and, when it
159	/// changes, flush any media still queued in their decoder or render buffers. The frame
160	/// returned by the read that bumps it is the first of the new timeline.
161	pub fn discontinuity(&self) -> u64 {
162		self.rewind.discontinuity
163	}
164
165	/// Read the next frame from the track.
166	///
167	/// This method handles timestamp decoding, group ordering, and latency management
168	/// automatically. It will skip groups that are too far behind to maintain the
169	/// configured latency target.
170	///
171	/// Returns `None` when the track has ended.
172	pub async fn read(&mut self) -> Result<Option<Frame>, F::Error> {
173		kio::wait(|waiter| self.poll_read(waiter)).await
174	}
175
176	/// Poll-based implementation of the read loop.
177	///
178	/// Uses a single waiter that gets registered on all relevant kio channels,
179	/// avoiding the need for `tokio::select!` or `FuturesUnordered`.
180	pub fn poll_read(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Frame>, F::Error>> {
181		// Grab any new groups from the track, recording whether the track is finished.
182		let finished = self.poll_read_finish(waiter)?.is_ready();
183
184		// On startup, we want to poll every pending group and advance self.current to the first with a frame.
185		if self.startup {
186			// NOTE: We loop in ascending order, so earlier groups will win the race.
187			for (i, group) in self.pending.iter_mut().enumerate() {
188				// We call poll_min_timestamp to try to buffer at least one frame per group.
189				// This returns Ready(Ok) if there is a buffered frame.
190				if !matches!(group.poll_min_timestamp(waiter, &self.format), Poll::Ready(Ok(_))) {
191					continue;
192				}
193
194				// Start reading from this group and skip any previous groups.
195				self.current = group.sequence;
196				self.startup = false;
197				self.pending.drain(0..i);
198				break;
199			}
200		}
201
202		loop {
203			// A newer group whose timestamps jumped backwards means the publisher reneged
204			// the buffered tail. Record the boundary and resume from the new epoch, then restart.
205			if self.poll_reset(waiter)? {
206				continue;
207			}
208
209			// Drop any reneged stragglers whose timestamps have since resolved them as old.
210			self.poll_classify(waiter)?;
211
212			// Return the next frame from the current group if possible.
213			// If the current group is finished or errored, advance to the next group.
214			while let Some(group) = self.pending.front_mut()
215				&& group.sequence <= self.current
216			{
217				match group.poll_read(waiter, &self.format) {
218					Poll::Ready(Ok(Some(frame))) => {
219						// Track the live edge (the max timestamp and the group that carries it) so a
220						// later backwards jump is detectable and the old epoch's tail is anchored.
221						let seq = group.group.sequence;
222						let ts = frame.timestamp;
223						if self.rewind.live_edge.is_none_or(|(_, high)| ts > high) {
224							self.rewind.live_edge = Some((seq, ts));
225						}
226						return Poll::Ready(Ok(Some(frame)));
227					}
228					// Still blocked on this group, don't skip it yet.
229					Poll::Pending => break,
230					Poll::Ready(Err(e)) => {
231						// Tell a relay group eviction/abort (skip) from a payload decode error
232						// (propagate). The moq_net group's own terminal state is the source of
233						// truth: an evicted/aborted group reports the transport error from
234						// poll_finished, while a malformed payload leaves the group live or
235						// cleanly finished. A decode error is real and the caller must see it,
236						// not have the group silently dropped.
237						if !group.poll_aborted(waiter) {
238							return Poll::Ready(Err(e));
239						}
240						// The group aged out of the relay cache (`Error::Old`) or was otherwise
241						// aborted. Any sequences between it and the next buffered group were
242						// evicted alongside it, so jump straight to that group instead of
243						// stepping one-by-one and then blocking on a sequence gap of groups
244						// that will never arrive.
245						tracing::warn!(error = ?e, "current group evicted; skipping to next buffered group");
246						self.pending.pop_front();
247						self.current = self.pending.front().map_or(self.current + 1, |g| g.sequence);
248					}
249					// Cleanly finished group: advance to the next sequence.
250					Poll::Ready(Ok(None)) => {
251						self.pending.pop_front();
252						self.current += 1;
253					}
254				}
255			}
256
257			// Get the current group's min timestamp (the reference for latency
258			// comparison) and its furthest presentation point (timestamp + duration).
259			let (oldest_timestamp, current_end) = if let Some(current) = self.pending.front_mut()
260				&& current.sequence <= self.current
261			{
262				match current.poll_min_timestamp(waiter, &self.format) {
263					Poll::Ready(Ok(ts)) => (Some(std::time::Duration::from(ts)), current.max_end),
264					_ => (None, None),
265				}
266			} else {
267				(None, None)
268			};
269
270			// Find the first newer group with data (our skip target) and where it starts.
271			let mut next_group = None;
272			for (i, group) in self.pending.iter_mut().enumerate() {
273				if group.sequence <= self.current {
274					continue;
275				}
276
277				if let Poll::Ready(Ok(ts)) = group.poll_min_timestamp(waiter, &self.format) {
278					next_group = Some((i, std::time::Duration::from(ts)));
279					break;
280				}
281			}
282
283			// Find the max timestamp across all newer groups.
284			let mut max_timestamp = std::time::Duration::ZERO;
285			for group in self.pending.iter_mut().rev() {
286				if group.sequence <= self.current {
287					break;
288				}
289
290				if let Poll::Ready(Ok(ts)) = group.poll_max_timestamp(waiter, &self.format) {
291					max_timestamp = max_timestamp.max(ts.into());
292					break; // We know older groups won't be newer than this.
293				}
294			}
295
296			let should_skip = if let Some((_, next_start)) = next_group {
297				if let Some(oldest) = oldest_timestamp {
298					// Current group is blocking. Skip if newer groups have pulled past
299					// the latency budget, or if the current group has already presented
300					// up to where the next group begins (duration coverage) so there's
301					// nothing left worth waiting for.
302					let over_latency = max_timestamp.saturating_sub(oldest) >= self.latency;
303					let covered = current_end.is_some_and(|end| end >= next_start);
304					over_latency || covered
305				} else {
306					// The current group can't produce a timestamp: either it's missing
307					// entirely -- a lower sequence the cache evicted, so `front` is already
308					// past `current` -- or it's finished/empty. With a newer group buffered,
309					// skip if the track is done OR the current sequence is simply gone. On a
310					// live track a buffered higher sequence means the missing one was evicted
311					// (the relay delivers in order), not merely late, so waiting is futile.
312					finished || self.pending.front().is_some_and(|g| g.sequence > self.current)
313				}
314			} else {
315				false
316			};
317
318			if let Some((new_idx, _)) = next_group
319				&& should_skip
320			{
321				self.pending.drain(0..new_idx);
322				let new_current = self.pending.front().map(|g| g.sequence).unwrap();
323
324				tracing::debug!(old = self.current, new = new_current, "skipping slow groups");
325
326				self.current = new_current;
327				continue;
328			}
329
330			if finished && self.pending.is_empty() {
331				return Poll::Ready(Ok(None));
332			}
333
334			return Poll::Pending;
335		}
336	}
337
338	// Reads any new groups from the track until we're completely finished.
339	//
340	// Returns Pending until all groups have been consumed.
341	fn poll_read_finish(&mut self, waiter: &kio::Waiter) -> Poll<Result<(), F::Error>> {
342		loop {
343			let Some(group) = ready!(self.track.poll_recv_group(waiter)?) else {
344				// Track is finished.
345				return Poll::Ready(Ok(()));
346			};
347
348			let reader = GroupBuffer::new(group);
349			let sequence = reader.group.sequence;
350
351			// Normally we drop anything behind the playback cursor. With an active reset the
352			// cursor isn't a valid floor: a late new-epoch group can sit below it. Defer to
353			// the boundary, admitting ambiguous groups so poll_classify can rule on them once
354			// their timestamps arrive.
355			let drop = match &self.rewind.boundary {
356				Some(reset) => match reset.by_sequence(sequence) {
357					Some(true) => true,                     // old epoch: reneged
358					Some(false) => sequence < self.current, // new epoch, but already played past
359					None => false,                          // ambiguous: admit, classify later
360				},
361				None => sequence < self.current,
362			};
363			if drop {
364				tracing::debug!(old = ?sequence, current = ?self.current, "skipping old group");
365				continue;
366			}
367
368			let idx = self
369				.pending
370				.partition_point(|g| g.group.sequence < reader.group.sequence);
371			self.pending.insert(idx, reader);
372		}
373	}
374
375	// Detect a publisher "rewind" and record the reneged boundary.
376	//
377	// A newer group (sequence climbs) whose first frame lands before the live edge (timestamp
378	// goes backwards) can only be an explicit reneg of the buffered tail. We record a [`Reset`]
379	// from `(live-edge group, rewound group, rewound timestamp)`, drop the buffered groups it
380	// can already prove stale, bump the discontinuity counter, and resume playback from the
381	// earliest survivor. Groups still ambiguous (a late new-epoch group vs. an old straggler)
382	// are kept and resolved by [`poll_classify`](Self::poll_classify) once their timestamps
383	// arrive.
384	//
385	// Returns true if a reset happened, signalling the caller to restart the read loop.
386	fn poll_reset(&mut self, waiter: &kio::Waiter) -> Result<bool, F::Error> {
387		let Some((prev_max, live_edge)) = self.rewind.live_edge else {
388			return Ok(false);
389		};
390
391		// Scan newer groups from the back (highest sequence first) for a rewind: a group whose
392		// timestamp went strictly backwards past the live edge. Checking only `back()` would
393		// miss a rewind that a higher-sequence group masks by having already caught back up
394		// (timestamp >= live edge) or by having no frame yet. Pending is sequence-sorted, so we
395		// take the highest-sequence group that actually rewound.
396		let reset = {
397			let mut found = None;
398			for group in self.pending.iter_mut().rev() {
399				// Once we reach the playback cursor, older groups can't rewind the timeline.
400				if group.group.sequence <= self.current {
401					break;
402				}
403
404				// Skip groups with no frame yet; a lower-sequence one may still have rewound.
405				let Poll::Ready(Ok(min)) = group.poll_min_timestamp(waiter, &self.format) else {
406					continue;
407				};
408
409				if min < live_edge {
410					found = Some(Reset {
411						prev_max,
412						group: group.group.sequence,
413						timestamp: min,
414					});
415					break;
416				}
417			}
418
419			let Some(reset) = found else {
420				return Ok(false);
421			};
422			reset
423		};
424
425		// Drop buffered groups the boundary can already prove are old-epoch. Ambiguous ones
426		// (no verdict by sequence, or timestamp not read yet) are kept for poll_classify.
427		self.pending.retain(|g| match reset.by_sequence(g.group.sequence) {
428			Some(stale) => !stale,
429			None => g.min_timestamp.is_none_or(|ts| !reset.is_stale(g.group.sequence, ts)),
430		});
431
432		self.rewind.discontinuity += 1;
433		tracing::debug!(
434			prev_max = reset.prev_max,
435			group = reset.group,
436			discontinuity = self.rewind.discontinuity,
437			"buffer reset: group timestamps rewound"
438		);
439		self.rewind.boundary = Some(reset);
440		// Resume from the earliest survivor; if none buffered yet, from the rewound group.
441		self.current = self.pending.front().map_or(reset.group, |g| g.group.sequence);
442		self.rewind.live_edge = Some((reset.group, reset.timestamp));
443
444		Ok(true)
445	}
446
447	// Resolve groups left ambiguous by a reset once their timestamps arrive.
448	//
449	// A group whose sequence falls in the reset's ambiguous span could be a late new-epoch
450	// group (keep) or an old straggler whose higher timestamp simply hadn't been seen at
451	// detection time (drop). We can only tell once it has a frame, so we re-check each loop
452	// iteration and drop the ones that resolve to stale.
453	fn poll_classify(&mut self, waiter: &kio::Waiter) -> Result<(), F::Error> {
454		let Some(reset) = self.rewind.boundary else {
455			return Ok(());
456		};
457
458		let mut i = 0;
459		while i < self.pending.len() {
460			let group = &mut self.pending[i];
461			// Only ambiguous-by-sequence groups need a timestamp verdict.
462			if reset.by_sequence(group.group.sequence).is_some() {
463				i += 1;
464				continue;
465			}
466
467			match group.poll_min_timestamp(waiter, &self.format) {
468				Poll::Ready(Ok(min)) if reset.is_stale(group.group.sequence, min) => {
469					self.pending.remove(i);
470				}
471				_ => i += 1,
472			}
473		}
474
475		Ok(())
476	}
477
478	/// Set the maximum latency tolerance.
479	pub fn set_latency(&mut self, latency: std::time::Duration) {
480		self.latency = latency;
481	}
482}
483
484/// Internal reader for a group of frames.
485///
486/// Handles two-phase frame reading (get FrameConsumer, then read all data),
487/// timestamp parsing, and min/max timestamp tracking for latency decisions.
488struct GroupBuffer {
489	group: moq_net::group::Consumer,
490
491	// The current frame index within the group.
492	index: usize,
493
494	// Read frames that haven't been consumed yet.
495	buffered: VecDeque<Frame>,
496
497	// The minimum timestamp in the group.
498	min_timestamp: Option<Timestamp>,
499
500	// The maximum timestamp in the group.
501	max_timestamp: Option<Timestamp>,
502
503	// The furthest presentation point reached so far, i.e. max(timestamp + duration).
504	// Equals the max timestamp when the container carries no per-frame duration.
505	// Stored as a wall-clock duration so cross-scale comparisons are cheap.
506	max_end: Option<std::time::Duration>,
507}
508
509impl GroupBuffer {
510	fn new(group: moq_net::group::Consumer) -> Self {
511		Self {
512			group,
513			index: 0,
514			buffered: VecDeque::new(),
515			max_timestamp: None,
516			min_timestamp: None,
517			max_end: None,
518		}
519	}
520
521	/// Poll for the next frame from this group.
522	fn poll_read<F: Container>(&mut self, waiter: &kio::Waiter, format: &F) -> Poll<Result<Option<Frame>, F::Error>> {
523		if let Some(frame) = self.buffered.pop_front() {
524			return Poll::Ready(Ok(Some(frame)));
525		}
526
527		match ready!(self.buffer_one(waiter, format)?) {
528			true => Poll::Ready(Ok(Some(self.buffered.pop_front().unwrap()))),
529			false => Poll::Ready(Ok(None)),
530		}
531	}
532
533	// Add one more frame to the buffer if possible.
534	//
535	// Returns false if the group is finished.
536	fn buffer_once<F: Container>(&mut self, waiter: &kio::Waiter, format: &F) -> Poll<Result<bool, F::Error>> {
537		let Some(frames) = ready!(format.poll_read(&mut self.group, waiter)?) else {
538			return Poll::Ready(Ok(false));
539		};
540
541		for mut frame in frames {
542			self.min_timestamp = Some(match self.min_timestamp {
543				Some(existing) => existing.min(frame.timestamp),
544				None => frame.timestamp,
545			});
546
547			self.max_timestamp = Some(match self.max_timestamp {
548				Some(existing) => existing.max(frame.timestamp),
549				None => frame.timestamp,
550			});
551
552			// Furthest presentation point, in wall-clock terms so timestamp and
553			// duration can be at different scales without extra conversions. A frame
554			// with no duration contributes only its timestamp.
555			let duration = frame.duration.map(std::time::Duration::from).unwrap_or_default();
556			let end = std::time::Duration::from(frame.timestamp) + duration;
557			self.max_end = Some(match self.max_end {
558				Some(existing) => existing.max(end),
559				None => end,
560			});
561
562			// First frame of a group is always a keyframe by protocol invariant; trust
563			// the container's flag otherwise so CMAF mid-group keyframes survive.
564			frame.keyframe = frame.keyframe || self.index == 0;
565			self.index += 1;
566
567			self.buffered.push_back(frame);
568		}
569
570		Poll::Ready(Ok(true))
571	}
572
573	fn buffer_one<F: Container>(&mut self, waiter: &kio::Waiter, format: &F) -> Poll<Result<bool, F::Error>> {
574		loop {
575			if !self.buffered.is_empty() {
576				return Poll::Ready(Ok(true));
577			}
578			if !ready!(self.buffer_once(waiter, format)?) {
579				return Poll::Ready(Ok(false));
580			}
581			// poll_read returned Some(vec![]): a wire frame decoded to no media
582			// frames, so loop and try again.
583		}
584	}
585
586	fn buffer_all<F: Container>(&mut self, waiter: &kio::Waiter, format: &F) -> Poll<Result<(), F::Error>> {
587		while ready!(self.buffer_once(waiter, format)?) {}
588		Poll::Ready(Ok(()))
589	}
590
591	/// Poll for the maximum timestamp in this group.
592	fn poll_max_timestamp<F: Container>(
593		&mut self,
594		waiter: &kio::Waiter,
595		format: &F,
596	) -> Poll<Result<Timestamp, F::Error>> {
597		// Keep reading more frames just to advance the max timestamp.
598		let _ = self.buffer_all(waiter, format)?;
599
600		if let Some(max) = self.max_timestamp {
601			return Poll::Ready(Ok(max));
602		}
603
604		if let Poll::Ready(_frames) = self.group.poll_finished(waiter)? {
605			return Poll::Ready(Err(moq_net::Error::Decode(moq_net::DecodeError::Short).into()));
606		}
607
608		Poll::Pending
609	}
610
611	fn poll_min_timestamp<F: Container>(
612		&mut self,
613		waiter: &kio::Waiter,
614		format: &F,
615	) -> Poll<Result<Timestamp, F::Error>> {
616		let _ = self.buffer_one(waiter, format)?;
617
618		if let Some(min) = self.min_timestamp {
619			return Poll::Ready(Ok(min));
620		}
621
622		if let Poll::Ready(_frames) = self.group.poll_finished(waiter)? {
623			return Poll::Ready(Err(moq_net::Error::Decode(moq_net::DecodeError::Short).into()));
624		}
625
626		Poll::Pending
627	}
628
629	/// True if the group's moq_net stream was reset/aborted (evicted, `Old`,
630	/// cancelled, ...), as opposed to still live or cleanly finished. Lets the
631	/// consumer tell a transport eviction from a payload decode error: the former
632	/// surfaces as a terminal transport error from `poll_finished`, the latter
633	/// leaves the group readable or finished.
634	fn poll_aborted(&mut self, waiter: &kio::Waiter) -> bool {
635		matches!(self.group.poll_finished(waiter), Poll::Ready(Err(_)))
636	}
637}
638
639impl std::ops::Deref for GroupBuffer {
640	type Target = moq_net::group::Consumer;
641
642	fn deref(&self) -> &Self::Target {
643		&self.group
644	}
645}
646
647#[cfg(test)]
648mod tests {
649	use super::Container as ContainerTrait;
650	use super::*;
651	use crate::catalog::hang::Container;
652	use std::time::Duration;
653
654	use bytes::Bytes;
655
656	/// Mint a standalone track for tests via a throwaway broadcast, since tracks are
657	/// born from their broadcast (no public `track::Producer::new`).
658	fn track_producer(
659		name: impl Into<std::sync::Arc<str>>,
660		info: impl Into<Option<moq_net::track::Info>>,
661	) -> moq_net::track::Producer {
662		moq_net::broadcast::Info::new()
663			.produce()
664			.create_track(name, info)
665			.unwrap()
666	}
667
668	fn ts(micros: u64) -> Timestamp {
669		Timestamp::from_micros(micros).unwrap()
670	}
671
672	/// Test-only container that round-trips a per-sample duration on the wire, so the
673	/// duration-based skip can be exercised without building a real CMAF init segment.
674	/// Each frame is `[timestamp_us: u64 LE][duration_us: u64 LE][payload]`.
675	struct DurationWire;
676
677	/// Encode a `[timestamp][duration][payload]` DurationWire frame.
678	fn encode_duration_frame(timestamp: Timestamp, duration: Timestamp) -> Vec<u8> {
679		let mut buf = Vec::with_capacity(18);
680		buf.extend_from_slice(&(timestamp.as_micros() as u64).to_le_bytes());
681		buf.extend_from_slice(&(duration.as_micros() as u64).to_le_bytes());
682		buf.extend_from_slice(&[0xDE, 0xAD]);
683		buf
684	}
685
686	impl ContainerTrait for DurationWire {
687		type Error = crate::Error;
688
689		fn write(&self, group: &mut moq_net::group::Producer, frames: &[Frame]) -> Result<(), Self::Error> {
690			// The duration tests write frames directly via `write_duration_frame`;
691			// this path just preserves the timestamp with an unknown duration.
692			for frame in frames {
693				group.write_frame(frame.timestamp, encode_duration_frame(frame.timestamp, ts(0)))?;
694			}
695			Ok(())
696		}
697
698		fn poll_read(
699			&self,
700			group: &mut moq_net::group::Consumer,
701			waiter: &kio::Waiter,
702		) -> Poll<Result<Option<Vec<Frame>>, Self::Error>> {
703			use bytes::Buf;
704
705			let Some(mut data) = ready!(group.poll_read_frame(waiter)?).map(|f| f.payload) else {
706				return Poll::Ready(Ok(None));
707			};
708
709			let timestamp = ts(data.get_u64_le());
710			let duration = ts(data.get_u64_le());
711			let payload = data.copy_to_bytes(data.remaining());
712
713			Poll::Ready(Ok(Some(vec![Frame {
714				timestamp,
715				payload,
716				keyframe: false,
717				duration: Some(duration),
718			}])))
719		}
720	}
721
722	/// Write one DurationWire frame (timestamp and duration in µs) into a group.
723	fn write_duration_frame(group: &mut moq_net::group::Producer, timestamp: Timestamp, duration: Timestamp) {
724		group
725			.write_frame(timestamp, encode_duration_frame(timestamp, duration))
726			.unwrap();
727	}
728
729	/// Write a finished group with explicit sequence and timestamps (Container::Legacy format).
730	fn write_group(track: &mut moq_net::track::Producer, sequence: u64, timestamps: &[Timestamp]) {
731		let mut group = track.create_group(moq_net::group::Info { sequence }).unwrap();
732		for &timestamp in timestamps {
733			let frame = Frame {
734				timestamp,
735				payload: Bytes::from_static(&[0xDE, 0xAD]),
736				keyframe: false,
737				duration: None,
738			};
739			Container::Legacy.write(&mut group, &[frame]).unwrap();
740		}
741		group.finish().unwrap();
742	}
743
744	/// Drain all available frames with a per-read timeout.
745	async fn read_all(consumer: &mut Consumer<Container>) -> Result<Vec<Frame>, crate::Error> {
746		let mut frames = Vec::new();
747		loop {
748			match tokio::time::timeout(Duration::from_millis(200), consumer.read()).await {
749				Ok(Ok(Some(frame))) => frames.push(frame),
750				Ok(Ok(None)) => break,
751				Ok(Err(e)) => return Err(e),
752				Err(_) => panic!(
753					"read_all: Consumer::read timed out after 200ms ({} frames collected so far)",
754					frames.len()
755				),
756			}
757		}
758		Ok(frames)
759	}
760
761	// ---- Basic Reading ----
762
763	#[tokio::test]
764	async fn read_single_group() {
765		let mut track = track_producer("test", hang::container::track_info());
766		let consumer_track = track.subscribe(None);
767		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
768
769		write_group(&mut track, 0, &[ts(0)]);
770		track.finish().unwrap();
771
772		let frames = read_all(&mut consumer).await.unwrap();
773		assert_eq!(frames.len(), 1);
774		assert_eq!(frames[0].timestamp, ts(0));
775		assert!(frames[0].keyframe);
776
777		// Next read returns None (track ended)
778		assert!(consumer.read().await.unwrap().is_none());
779	}
780
781	#[tokio::test]
782	async fn read_multiple_frames_single_group() {
783		let mut track = track_producer("test", hang::container::track_info());
784		let consumer_track = track.subscribe(None);
785		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
786
787		write_group(&mut track, 0, &[ts(0), ts(33_000), ts(66_000)]);
788		track.finish().unwrap();
789
790		let frames = read_all(&mut consumer).await.unwrap();
791		assert_eq!(frames.len(), 3);
792		assert_eq!(frames[0].timestamp, ts(0));
793		assert_eq!(frames[1].timestamp, ts(33_000));
794		assert_eq!(frames[2].timestamp, ts(66_000));
795
796		assert!(frames[0].keyframe);
797	}
798
799	#[tokio::test]
800	async fn read_multiple_groups_within_latency() {
801		let mut track = track_producer("test", hang::container::track_info());
802		let consumer_track = track.subscribe(None);
803		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
804
805		// 5 groups, 20ms spacing. Total span = 80ms, well within 500ms latency.
806		for i in 0..5u64 {
807			write_group(&mut track, i, &[ts(i * 20_000)]);
808		}
809		track.finish().unwrap();
810
811		let frames = read_all(&mut consumer).await.unwrap();
812		assert_eq!(frames.len(), 5);
813	}
814
815	// ---- Latency Skipping ----
816
817	#[tokio::test]
818	async fn latency_skip_delivers_recent_groups() {
819		tokio::time::pause();
820		let mut track = track_producer("test", hang::container::track_info());
821		let consumer_track = track.subscribe(None);
822		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
823
824		// Group 0: 5 frames, NOT finished (blocks consumer)
825		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
826		for f in 0..5u64 {
827			Container::Legacy
828				.write(
829					&mut group0,
830					&[Frame {
831						timestamp: ts(f * 2_000),
832						payload: Bytes::from_static(&[0xDE, 0xAD]),
833						keyframe: false,
834						duration: None,
835					}],
836				)
837				.unwrap();
838		}
839
840		// Groups 1-19: finished, 15ms spacing, 5 frames each
841		for g in 1..20u64 {
842			let timestamps: Vec<_> = (0..5).map(|f| ts(g * 15_000 + f * 2_000)).collect();
843			write_group(&mut track, g, &timestamps);
844		}
845		track.finish().unwrap();
846
847		// Finish group 0 after consumer has had time to accumulate pending groups
848		let finisher = tokio::spawn(async move {
849			tokio::time::sleep(Duration::from_millis(50)).await;
850			group0.finish().unwrap();
851		});
852
853		let frames = read_all(&mut consumer).await.unwrap();
854		// Group 0's 5 frames + some later groups (earlier ones skipped by latency)
855		assert!(frames.len() >= 25, "Expected >= 25 frames, got {}", frames.len());
856		finisher.await.expect("finisher task panicked");
857	}
858
859	#[tokio::test]
860	async fn zero_latency_skips_aggressively() {
861		tokio::time::pause();
862		let mut track = track_producer("test", hang::container::track_info());
863		let consumer_track = track.subscribe(None);
864		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::ZERO);
865
866		// Group 0 at ts 0 keeps timestamps monotonic with sequence (groups 1-9 follow at
867		// g*50 ms), so the test exercises latency skipping and not rewind detection.
868		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
869		Container::Legacy
870			.write(
871				&mut group0,
872				&[Frame {
873					timestamp: ts(0),
874					payload: Bytes::from_static(&[0xDE, 0xAD]),
875					keyframe: false,
876					duration: None,
877				}],
878			)
879			.unwrap();
880
881		for g in 1..10u64 {
882			let timestamps: Vec<_> = (0..3).map(|f| ts(g * 50_000 + f * 5_000)).collect();
883			write_group(&mut track, g, &timestamps);
884		}
885		track.finish().unwrap();
886
887		let finisher = tokio::spawn(async move {
888			tokio::time::sleep(Duration::from_millis(50)).await;
889			group0.finish().unwrap();
890		});
891
892		let frames = read_all(&mut consumer).await.unwrap();
893		assert_eq!(frames.len(), 28, "Expected group 0 frame + groups 1-9");
894		assert!(!frames.is_empty(), "Expected at least some frames");
895		finisher.await.expect("finisher task panicked");
896	}
897
898	#[tokio::test]
899	async fn latency_skip_correctness() {
900		tokio::time::pause();
901		let mut track = track_producer("test", hang::container::track_info());
902		let consumer_track = track.subscribe(None);
903		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
904
905		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
906		Container::Legacy
907			.write(
908				&mut group0,
909				&[Frame {
910					timestamp: ts(0),
911					payload: Bytes::from_static(&[0xDE, 0xAD]),
912					keyframe: false,
913					duration: None,
914				}],
915			)
916			.unwrap();
917
918		for g in 1..10u64 {
919			write_group(&mut track, g, &[ts(g * 30_000)]);
920		}
921		track.finish().unwrap();
922
923		let finisher = tokio::spawn(async move {
924			tokio::time::sleep(Duration::from_millis(50)).await;
925			group0.finish().unwrap();
926		});
927
928		let frames = read_all(&mut consumer).await.unwrap();
929		assert!(!frames.is_empty(), "Expected at least some frames");
930		assert_eq!(frames.len(), 10, "Expected group 0 frame + groups 1-9");
931		assert_eq!(frames[0].timestamp, ts(0));
932
933		for i in 1..10u64 {
934			assert_eq!(frames[i as usize].timestamp, ts(i * 30_000));
935		}
936		finisher.await.expect("finisher task panicked");
937	}
938
939	// ---- Rewind / reneg ----
940
941	/// The reset boundary classifies out-of-order groups by `(sequence, timestamp)`.
942	/// Old epoch peaked at group 55 (ts 100); group 58 rewound to ts 90.
943	#[test]
944	fn reset_classifies_out_of_order_groups() {
945		let reset = Reset {
946			prev_max: 55,
947			group: 58,
948			timestamp: ts(90),
949		};
950
951		// Late new-epoch gap-filler: sequence in (55, 58), ts below the rewind. Keep.
952		assert!(!reset.is_stale(57, ts(88)));
953		// Old straggler from before the peak (low sequence). Drop, even though its ts (86)
954		// is below the rewind — sequence is what separates it from group 57.
955		assert!(reset.is_stale(52, ts(86)));
956		// Old straggler in the gap whose higher ts hadn't arrived at detection. Drop.
957		assert!(reset.is_stale(56, ts(105)));
958		// At or after the rewound group: new epoch. Keep.
959		assert!(!reset.is_stale(58, ts(90)));
960		assert!(!reset.is_stale(59, ts(92)));
961		// At or before the old peak: old epoch. Drop.
962		assert!(reset.is_stale(55, ts(100)));
963	}
964
965	/// A new-epoch group that arrives out of order *below* the resume point is kept and
966	/// played, not dropped — the bug a plain "floor = detection group" would have.
967	#[tokio::test]
968	async fn reset_keeps_out_of_order_new_group() {
969		tokio::time::pause();
970		let mut track = track_producer("test", hang::container::track_info());
971		let consumer_track = track.subscribe(None);
972		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
973
974		// Old epoch, played forward until the live edge passes the rewind point.
975		write_group(&mut track, 0, &[ts(0)]);
976		write_group(&mut track, 1, &[ts(100_000)]);
977		write_group(&mut track, 2, &[ts(200_000)]);
978		// New epoch's later group (seq 5, ts 3 ms) arrives first and triggers the reset.
979		write_group(&mut track, 5, &[ts(3_000)]);
980
981		// Its earlier gap-fillers (seq 3, 4) land after the reset, below the resume point.
982		let finisher = tokio::spawn(async move {
983			tokio::time::sleep(Duration::from_millis(50)).await;
984			write_group(&mut track, 3, &[ts(1_000)]);
985			write_group(&mut track, 4, &[ts(2_000)]);
986			track.finish().unwrap();
987		});
988
989		let frames = read_all(&mut consumer).await.unwrap();
990		let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
991
992		// Old epoch played before the reset, and all three new-epoch groups survived —
993		// including the two out-of-order gap-fillers that arrived below the resume point.
994		assert!(micros.contains(&100_000), "old epoch played before the reset");
995		assert!(
996			micros.contains(&1_000) && micros.contains(&2_000) && micros.contains(&3_000),
997			"out-of-order new-epoch groups kept, got {micros:?}"
998		);
999		assert_eq!(consumer.discontinuity(), 1, "one rewind detected");
1000		finisher.await.expect("finisher task panicked");
1001	}
1002
1003	/// A rewind is detected even when a higher-sequence group has already caught back up past
1004	/// the live edge (so the newest pending group looks forward). Scanning only `back()` would
1005	/// miss the lower-sequence rewound group and play the reneged tail without a discontinuity.
1006	#[tokio::test]
1007	async fn reset_detected_behind_forward_newest_group() {
1008		tokio::time::pause();
1009		let mut track = track_producer("test", hang::container::track_info());
1010		let consumer_track = track.subscribe(None);
1011		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
1012
1013		// Old timeline, played to a live edge of 200 ms.
1014		write_group(&mut track, 0, &[ts(0)]);
1015		write_group(&mut track, 1, &[ts(100_000)]);
1016		write_group(&mut track, 2, &[ts(200_000)]);
1017		// Group 6 (highest sequence) is forward of the live edge, masking...
1018		write_group(&mut track, 6, &[ts(250_000)]);
1019		// ...group 5, a lower-sequence group that rewound below it.
1020		write_group(&mut track, 5, &[ts(50_000)]);
1021		track.finish().unwrap();
1022
1023		let frames = read_all(&mut consumer).await.unwrap();
1024		let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
1025
1026		assert_eq!(
1027			consumer.discontinuity(),
1028			1,
1029			"rewind detected behind a forward newest group"
1030		);
1031		assert!(micros.contains(&50_000), "resumed at the rewound group, got {micros:?}");
1032		assert!(
1033			!micros.contains(&200_000),
1034			"the reneged tail was dropped, got {micros:?}"
1035		);
1036	}
1037
1038	/// A newer group whose timestamps jump backwards past the buffered tail drops the
1039	/// reneged groups and resumes from the rewound group. Models a voice agent that
1040	/// runs ahead of playback and then interrupts to start a new utterance.
1041	#[tokio::test]
1042	async fn backwards_timestamp_resets_buffer() {
1043		tokio::time::pause();
1044		let mut track = track_producer("test", hang::container::track_info());
1045		let consumer_track = track.subscribe(None);
1046		// Large latency so the slow-group skip never fires; isolate the rewind path.
1047		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
1048
1049		// Publisher runs ahead: groups 0-4 at 0, 100, 200, 300, 400 ms.
1050		for i in 0..5u64 {
1051			write_group(&mut track, i, &[ts(i * 100_000)]);
1052		}
1053		// Then it reneges and rewinds: group 5 restarts the timeline at 0 ms.
1054		write_group(&mut track, 5, &[ts(0), ts(20_000)]);
1055		track.finish().unwrap();
1056
1057		let frames = read_all(&mut consumer).await.unwrap();
1058		let timestamps: Vec<_> = frames.iter().map(|f| f.timestamp).collect();
1059
1060		// We play forward until the live edge passes the rewind point (through 100 ms), then
1061		// the rewind drops the buffered-ahead groups (200/300/400 ms) and resumes at group 5.
1062		assert_eq!(timestamps, vec![ts(0), ts(100_000), ts(0), ts(20_000)]);
1063		assert_eq!(consumer.discontinuity(), 1);
1064	}
1065
1066	/// Rewind detection is always on: a backwards group timestamp resets the buffer with no
1067	/// configuration. Here group 2 rewinds the timeline and bumps the discontinuity counter.
1068	#[tokio::test]
1069	async fn backwards_timestamp_always_resets() {
1070		let mut track = track_producer("test", hang::container::track_info());
1071		let consumer_track = track.subscribe(None);
1072		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
1073
1074		write_group(&mut track, 0, &[ts(0)]);
1075		write_group(&mut track, 1, &[ts(500_000)]);
1076		write_group(&mut track, 2, &[ts(0)]); // rewind
1077		track.finish().unwrap();
1078
1079		let frames = read_all(&mut consumer).await.unwrap();
1080		let timestamps: Vec<_> = frames.iter().map(|f| f.timestamp).collect();
1081
1082		assert_eq!(timestamps, vec![ts(0), ts(500_000), ts(0)]);
1083		assert_eq!(consumer.discontinuity(), 1, "the backwards group triggered a reset");
1084	}
1085
1086	// ---- Empty payloads ----
1087
1088	/// Write one frame with an empty payload: a marker saying content stops at
1089	/// `timestamp`, carrying no media.
1090	fn write_marker(group: &mut moq_net::group::Producer, timestamp: Timestamp) {
1091		let frame = Frame {
1092			timestamp,
1093			payload: Bytes::new(),
1094			keyframe: false,
1095			duration: None,
1096		};
1097		Container::Legacy.write(group, &[frame]).unwrap();
1098	}
1099
1100	/// An empty payload carries no media, so it's skipped rather than surfaced as a
1101	/// frame or raised as an error. A marker can sit anywhere -- mid-group (a gap) or
1102	/// last (a group's end) -- and a publisher emitting them must not break us.
1103	#[tokio::test]
1104	async fn empty_payload_is_skipped() {
1105		let mut track = track_producer("test", hang::container::track_info());
1106		let consumer_track = track.subscribe(None);
1107		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1108
1109		let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1110		let media = |timestamp| Frame {
1111			timestamp,
1112			payload: Bytes::from_static(&[0xDE, 0xAD]),
1113			keyframe: false,
1114			duration: None,
1115		};
1116		Container::Legacy.write(&mut group, &[media(ts(0))]).unwrap();
1117		write_marker(&mut group, ts(16_000)); // mid-group gap marker
1118		Container::Legacy.write(&mut group, &[media(ts(33_000))]).unwrap();
1119		write_marker(&mut group, ts(50_000)); // the group's end
1120		group.finish().unwrap();
1121		track.finish().unwrap();
1122
1123		let frames = read_all(&mut consumer).await.unwrap();
1124		assert_eq!(frames.len(), 2, "markers are not surfaced as media");
1125		assert_eq!(frames[0].timestamp, ts(0));
1126		assert_eq!(frames[1].timestamp, ts(33_000));
1127	}
1128
1129	/// Reading a marker consumes its frame, so a run of them makes progress and the
1130	/// consumer reaches the next group instead of spinning. `read_all` times out per
1131	/// read, so a stall or an infinite loop fails this rather than hanging forever.
1132	#[tokio::test]
1133	async fn consecutive_markers_do_not_stall() {
1134		let mut track = track_producer("test", hang::container::track_info());
1135		let consumer_track = track.subscribe(None);
1136		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1137
1138		let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1139		Container::Legacy
1140			.write(
1141				&mut group,
1142				&[Frame {
1143					timestamp: ts(0),
1144					payload: Bytes::from_static(&[0xDE, 0xAD]),
1145					keyframe: false,
1146					duration: None,
1147				}],
1148			)
1149			.unwrap();
1150		for i in 1..5u64 {
1151			write_marker(&mut group, ts(i * 1_000));
1152		}
1153		group.finish().unwrap();
1154		write_group(&mut track, 1, &[ts(100_000)]);
1155		track.finish().unwrap();
1156
1157		let frames = read_all(&mut consumer).await.unwrap();
1158		let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
1159		assert_eq!(micros, vec![0, 100_000], "markers skipped, next group reached");
1160	}
1161
1162	// ---- Group Ordering ----
1163
1164	#[tokio::test]
1165	async fn groups_delivered_in_sequence_order() {
1166		tokio::time::pause();
1167		let mut track = track_producer("test", hang::container::track_info());
1168		let consumer_track = track.subscribe(None);
1169		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1170
1171		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1172		Container::Legacy
1173			.write(
1174				&mut group0,
1175				&[Frame {
1176					timestamp: ts(0),
1177					payload: Bytes::from_static(&[0xDE, 0xAD]),
1178					keyframe: false,
1179					duration: None,
1180				}],
1181			)
1182			.unwrap();
1183
1184		write_group(&mut track, 2, &[ts(60_000)]);
1185		write_group(&mut track, 1, &[ts(30_000)]);
1186		track.finish().unwrap();
1187
1188		let finisher = tokio::spawn(async move {
1189			tokio::time::sleep(Duration::from_millis(10)).await;
1190			group0.finish().unwrap();
1191		});
1192
1193		let frames = read_all(&mut consumer).await.unwrap();
1194		assert_eq!(frames.len(), 3);
1195		assert_eq!(frames[0].timestamp, ts(0));
1196		assert_eq!(frames[1].timestamp, ts(30_000));
1197		assert_eq!(frames[2].timestamp, ts(60_000));
1198		finisher.await.expect("finisher task panicked");
1199	}
1200
1201	#[tokio::test]
1202	async fn adjacent_group_flushed_immediately() {
1203		let mut track = track_producer("test", hang::container::track_info());
1204		let consumer_track = track.subscribe(None);
1205		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1206
1207		write_group(&mut track, 0, &[ts(0)]);
1208		write_group(&mut track, 1, &[ts(30_000)]);
1209		track.finish().unwrap();
1210
1211		let frames = read_all(&mut consumer).await.unwrap();
1212		assert_eq!(frames.len(), 2);
1213		assert_eq!(frames[0].timestamp, ts(0));
1214		assert_eq!(frames[1].timestamp, ts(30_000));
1215	}
1216
1217	// ---- B-frames ----
1218
1219	#[tokio::test]
1220	async fn bframes_within_group() {
1221		let mut track = track_producer("test", hang::container::track_info());
1222		let consumer_track = track.subscribe(None);
1223		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1224
1225		write_group(&mut track, 0, &[ts(0), ts(66_000), ts(33_000)]);
1226		track.finish().unwrap();
1227
1228		let frames = read_all(&mut consumer).await.unwrap();
1229		assert_eq!(frames.len(), 3);
1230		assert_eq!(frames[0].timestamp, ts(0));
1231		assert_eq!(frames[1].timestamp, ts(66_000));
1232		assert_eq!(frames[2].timestamp, ts(33_000));
1233	}
1234
1235	// ---- Track Lifecycle ----
1236
1237	#[tokio::test]
1238	async fn empty_track_returns_none() {
1239		tokio::time::pause();
1240		let mut track = track_producer("test", hang::container::track_info());
1241		let consumer_track = track.subscribe(None);
1242		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1243
1244		track.finish().unwrap();
1245
1246		let result = tokio::time::timeout(Duration::from_millis(200), consumer.read()).await;
1247		match result {
1248			Ok(Ok(None)) => {} // expected: track ended
1249			Ok(Ok(Some(_))) => panic!("expected None for empty track, got Some"),
1250			Ok(Err(e)) => panic!("expected None for empty track, got error: {e}"),
1251			Err(_) => panic!("should not hang on empty track"),
1252		}
1253	}
1254
1255	#[tokio::test]
1256	async fn track_closed_with_error() {
1257		tokio::time::pause();
1258		let mut track = track_producer("test", hang::container::track_info());
1259		let consumer_track = track.subscribe(None);
1260		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1261
1262		write_group(&mut track, 0, &[ts(0)]);
1263		track.abort(moq_net::Error::Cancel).unwrap();
1264
1265		let result = tokio::time::timeout(Duration::from_millis(500), async {
1266			let mut frames = Vec::new();
1267			while let Ok(Some(frame)) = consumer.read().await {
1268				frames.push(frame);
1269			}
1270			frames
1271		})
1272		.await;
1273
1274		assert!(result.is_ok(), "Consumer should not hang after track error");
1275	}
1276
1277	// ---- Gap Recovery ----
1278
1279	#[tokio::test]
1280	async fn gap_in_group_sequence_recovery() {
1281		let mut track = track_producer("test", hang::container::track_info());
1282		let consumer_track = track.subscribe(None);
1283		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
1284
1285		write_group(&mut track, 0, &[ts(0), ts(20_000)]);
1286		write_group(&mut track, 1, &[ts(40_000), ts(60_000)]);
1287		write_group(&mut track, 3, &[ts(120_000), ts(140_000)]);
1288		write_group(&mut track, 4, &[ts(160_000), ts(180_000)]);
1289		write_group(&mut track, 5, &[ts(200_000), ts(220_000)]);
1290		write_group(&mut track, 6, &[ts(240_000), ts(260_000)]);
1291		track.finish().unwrap();
1292
1293		let frames = read_all(&mut consumer).await.unwrap();
1294		assert!(frames.len() >= 4, "Expected >= 4 frames, got {}", frames.len());
1295	}
1296
1297	#[tokio::test]
1298	async fn gap_at_start_of_sequence() {
1299		let mut track = track_producer("test", hang::container::track_info());
1300		let consumer_track = track.subscribe(None);
1301		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(80));
1302
1303		write_group(&mut track, 5, &[ts(0), ts(20_000)]);
1304		write_group(&mut track, 7, &[ts(80_000), ts(100_000)]);
1305		write_group(&mut track, 8, &[ts(120_000), ts(140_000)]);
1306		write_group(&mut track, 9, &[ts(160_000), ts(180_000)]);
1307		track.finish().unwrap();
1308
1309		let frames = read_all(&mut consumer).await.unwrap();
1310		assert!(frames.len() >= 4, "Expected >= 4 frames, got {}", frames.len());
1311	}
1312
1313	// ---- Eviction recovery (pause/resume) ----
1314
1315	/// A group that aged out of the relay cache (aborted with `Error::Old`) while the
1316	/// consumer was parked on it must not hang the consumer: reading it errors, and
1317	/// the consumer skips the gap to the next live group even though the track is NOT
1318	/// finished. This is the resume-from-pause path (the recorder stops reading, the
1319	/// group + the sequences after it evict, then it resumes).
1320	#[tokio::test]
1321	async fn evicted_group_with_gap_skips_to_live() {
1322		let mut track = track_producer("test", hang::container::track_info());
1323		let consumer_track = track.subscribe(None);
1324		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
1325
1326		// Group 0: a frame the consumer reads, positioning it there.
1327		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1328		Container::Legacy
1329			.write(
1330				&mut group0,
1331				&[Frame {
1332					timestamp: ts(0),
1333					payload: Bytes::from_static(&[0xDE, 0xAD]),
1334					keyframe: false,
1335					duration: None,
1336				}],
1337			)
1338			.unwrap();
1339		let first = consumer.read().await.unwrap().unwrap();
1340		assert_eq!(first.timestamp, ts(0));
1341
1342		// A live group arrives far ahead -- sequences 1..4 never come (evicted). The
1343		// track stays OPEN (not finished), the failure mode that used to hang.
1344		write_group(&mut track, 5, &[ts(150_000)]);
1345
1346		// Group 0 ages out of the cache (the relay aborts it on eviction).
1347		group0.abort(moq_net::Error::Old).unwrap();
1348
1349		// Must skip the evicted group + the gap and reach the live group, without
1350		// hanging on a track that never finishes.
1351		let next = tokio::time::timeout(Duration::from_secs(1), consumer.read())
1352			.await
1353			.expect("consumer hung on an evicted group / gap")
1354			.unwrap()
1355			.unwrap();
1356		assert_eq!(next.timestamp, ts(150_000), "skipped the evicted gap to the live group");
1357	}
1358
1359	/// A missing (evicted) sequence with a newer group buffered must be skipped even
1360	/// while the track is still LIVE -- not only once it's finished. This is the
1361	/// recorder resume stall: `current` points at a sequence the cache dropped, a
1362	/// higher group is buffered, and the track never finishes.
1363	#[tokio::test]
1364	async fn missing_sequence_skips_on_live_track() {
1365		let mut track = track_producer("test", hang::container::track_info());
1366		let consumer_track = track.subscribe(None);
1367		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
1368
1369		// Group 0, then group 2 -- sequence 1 is missing (evicted) and never arrives.
1370		// The track is NOT finished (live), the case that used to hang.
1371		write_group(&mut track, 0, &[ts(0), ts(20_000)]);
1372		write_group(&mut track, 2, &[ts(200_000)]);
1373
1374		// Reading must reach group 2 across the gap instead of waiting forever for 1.
1375		let reached = tokio::time::timeout(Duration::from_secs(1), async {
1376			loop {
1377				let frame = consumer.read().await.unwrap().unwrap();
1378				if frame.timestamp == ts(200_000) {
1379					return;
1380				}
1381			}
1382		})
1383		.await;
1384		assert!(reached.is_ok(), "consumer hung on a missing sequence on a live track");
1385	}
1386
1387	// ---- Decode errors ----
1388
1389	/// A container that decodes each frame's payload as an 8-byte LE microsecond
1390	/// timestamp, but treats a `FAIL` payload as a malformed frame. Lets a test put a
1391	/// decodable frame first (so startup selects the group) and a decode failure after.
1392	struct FailingDecode;
1393
1394	impl ContainerTrait for FailingDecode {
1395		type Error = crate::Error;
1396
1397		fn write(&self, group: &mut moq_net::group::Producer, frames: &[Frame]) -> Result<(), Self::Error> {
1398			for frame in frames {
1399				group.write_frame(moq_net::Timestamp::ZERO, frame.payload.clone())?;
1400			}
1401			Ok(())
1402		}
1403
1404		fn poll_read(
1405			&self,
1406			group: &mut moq_net::group::Consumer,
1407			waiter: &kio::Waiter,
1408		) -> Poll<Result<Option<Vec<Frame>>, Self::Error>> {
1409			use bytes::Buf;
1410
1411			let Some(mut data) = ready!(group.poll_read_frame(waiter)?).map(|f| f.payload) else {
1412				return Poll::Ready(Ok(None));
1413			};
1414			if data.as_ref() == b"FAIL" {
1415				return Poll::Ready(Err(crate::Error::UnknownFormat("malformed payload".into())));
1416			}
1417			Poll::Ready(Ok(Some(vec![Frame {
1418				timestamp: ts(data.get_u64_le()),
1419				payload: Bytes::new(),
1420				keyframe: false,
1421				duration: None,
1422			}])))
1423		}
1424	}
1425
1426	/// A decode error on a cleanly-finished group must propagate to the caller, not be
1427	/// mistaken for a relay eviction and silently skipped. Eviction-skip only fires when
1428	/// the group's stream was actually aborted.
1429	#[tokio::test]
1430	async fn decode_error_propagates() {
1431		tokio::time::pause();
1432		let mut track = track_producer("test", None);
1433		let consumer_track = track.subscribe(None);
1434		let mut consumer = Consumer::new(consumer_track, FailingDecode);
1435
1436		// A decodable frame first (so startup selects the group), then a malformed one.
1437		let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1438		group
1439			.write_frame(moq_net::Timestamp::ZERO, Bytes::from(0u64.to_le_bytes().to_vec()))
1440			.unwrap();
1441		group
1442			.write_frame(moq_net::Timestamp::ZERO, Bytes::from_static(b"FAIL"))
1443			.unwrap();
1444		group.finish().unwrap();
1445		track.finish().unwrap();
1446
1447		// The first frame decodes; the malformed second frame must surface as an error.
1448		let first = consumer.read().await;
1449		assert!(matches!(first, Ok(Some(_))), "first frame should decode, got {first:?}");
1450
1451		let second = tokio::time::timeout(Duration::from_millis(200), consumer.read())
1452			.await
1453			.expect("consumer hung on a decode error");
1454		assert!(
1455			matches!(second, Err(crate::Error::UnknownFormat(_))),
1456			"decode error must propagate, got {second:?}"
1457		);
1458	}
1459
1460	// ---- Frame Decoding ----
1461
1462	#[tokio::test]
1463	async fn frame_timestamp_and_index_decoding() {
1464		let mut track = track_producer("test", hang::container::track_info());
1465		let consumer_track = track.subscribe(None);
1466		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1467
1468		write_group(&mut track, 0, &[ts(0), ts(33_333), ts(66_666)]);
1469		track.finish().unwrap();
1470
1471		let frames = read_all(&mut consumer).await.unwrap();
1472		assert_eq!(frames.len(), 3);
1473
1474		assert_eq!(frames[0].timestamp, ts(0));
1475		assert!(frames[0].keyframe);
1476
1477		assert_eq!(frames[1].timestamp, ts(33_333));
1478
1479		assert_eq!(frames[2].timestamp, ts(66_666));
1480	}
1481
1482	#[tokio::test]
1483	async fn frame_payload_preserved() {
1484		let mut track = track_producer("test", hang::container::track_info());
1485		let consumer_track = track.subscribe(None);
1486		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1487
1488		let payload_bytes = vec![0x01, 0x02, 0x03, 0x04, 0x05];
1489		let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1490		Container::Legacy
1491			.write(
1492				&mut group,
1493				&[Frame {
1494					timestamp: ts(0),
1495					payload: Bytes::from(payload_bytes.clone()),
1496
1497					keyframe: false,
1498					duration: None,
1499				}],
1500			)
1501			.unwrap();
1502		group.finish().unwrap();
1503		track.finish().unwrap();
1504
1505		let frames = read_all(&mut consumer).await.unwrap();
1506		assert_eq!(frames.len(), 1);
1507
1508		use bytes::Buf;
1509		let mut received = Vec::new();
1510		let mut payload = frames[0].payload.clone();
1511		while payload.has_remaining() {
1512			received.push(payload.get_u8());
1513		}
1514		assert_eq!(received, payload_bytes);
1515	}
1516
1517	// ---- Regression ----
1518
1519	#[tokio::test]
1520	async fn no_infinite_loop_with_buffered_frames() {
1521		tokio::time::pause();
1522		let mut track = track_producer("test", hang::container::track_info());
1523		let consumer_track = track.subscribe(None);
1524		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
1525
1526		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1527		Container::Legacy
1528			.write(
1529				&mut group0,
1530				&[Frame {
1531					timestamp: ts(0),
1532					payload: Bytes::from_static(&[0xDE, 0xAD]),
1533					keyframe: false,
1534					duration: None,
1535				}],
1536			)
1537			.unwrap();
1538
1539		write_group(&mut track, 1, &[ts(100_000)]);
1540
1541		let finisher = tokio::spawn(async move {
1542			tokio::time::sleep(Duration::from_millis(20)).await;
1543			// Write group 2: recv_group fires, drops current buffer_until for group 1
1544			write_group(&mut track, 2, &[ts(200_000)]);
1545			tokio::time::sleep(Duration::from_millis(20)).await;
1546			group0.finish().unwrap();
1547			track.finish().unwrap();
1548		});
1549
1550		let frames = tokio::time::timeout(Duration::from_secs(2), async {
1551			let mut frames = Vec::new();
1552			while let Some(frame) = consumer.read().await.unwrap() {
1553				frames.push(frame);
1554			}
1555			frames
1556		})
1557		.await
1558		.expect("consumer hung — possible infinite loop regression");
1559
1560		assert_eq!(frames.len(), 3);
1561		finisher.await.expect("finisher task panicked");
1562	}
1563
1564	// ---- Edge Cases ----
1565
1566	#[tokio::test]
1567	async fn large_timestamps() {
1568		let mut track = track_producer("test", hang::container::track_info());
1569		let consumer_track = track.subscribe(None);
1570		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(3700));
1571
1572		let one_hour = 3_600_000_000u64;
1573		write_group(&mut track, 0, &[ts(one_hour)]);
1574		track.finish().unwrap();
1575
1576		let frames = read_all(&mut consumer).await.unwrap();
1577		assert_eq!(frames.len(), 1);
1578		assert_eq!(frames[0].timestamp, ts(one_hour));
1579		assert_eq!(frames[0].timestamp.as_micros(), one_hour as u128);
1580	}
1581
1582	#[tokio::test]
1583	async fn set_latency_changes_behavior() {
1584		let mut track = track_producer("test", hang::container::track_info());
1585		let consumer_track = track.subscribe(None);
1586		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
1587
1588		write_group(&mut track, 0, &[ts(0)]);
1589		track.finish().unwrap();
1590
1591		let frame = consumer.read().await.unwrap().unwrap();
1592		assert_eq!(frame.timestamp, ts(0));
1593
1594		consumer.set_latency(Duration::from_millis(100));
1595
1596		assert!(consumer.read().await.unwrap().is_none());
1597	}
1598
1599	#[tokio::test]
1600	async fn max_timestamp_tracks_through_bframes() {
1601		tokio::time::pause();
1602		let mut track = track_producer("test", hang::container::track_info());
1603		let consumer_track = track.subscribe(None);
1604		// latency must exceed (group1_max - group0_min) = 100ms - 0ms = 100ms
1605		// to avoid the latency skip and test B-frame timestamp tracking.
1606		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(110));
1607
1608		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1609		for &timestamp in &[ts(0), ts(66_000), ts(33_000)] {
1610			Container::Legacy
1611				.write(
1612					&mut group0,
1613					&[Frame {
1614						timestamp,
1615						payload: Bytes::from_static(&[0xDE, 0xAD]),
1616						keyframe: false,
1617						duration: None,
1618					}],
1619				)
1620				.unwrap();
1621		}
1622
1623		write_group(&mut track, 1, &[ts(100_000)]);
1624		track.finish().unwrap();
1625
1626		let finisher = tokio::spawn(async move {
1627			tokio::time::sleep(Duration::from_millis(50)).await;
1628			group0.finish().unwrap();
1629		});
1630
1631		let frames = tokio::time::timeout(Duration::from_secs(2), async {
1632			let mut frames = Vec::new();
1633			while let Some(frame) = consumer.read().await.unwrap() {
1634				frames.push(frame);
1635			}
1636			frames
1637		})
1638		.await
1639		.expect("consumer hung — max_timestamp regression");
1640
1641		assert_eq!(frames.len(), 4, "Expected all 4 frames, got {}", frames.len());
1642		assert_eq!(frames[0].timestamp, ts(0));
1643		assert_eq!(frames[1].timestamp, ts(66_000));
1644		assert_eq!(frames[2].timestamp, ts(33_000));
1645		assert_eq!(frames[3].timestamp, ts(100_000));
1646		finisher.await.expect("finisher task panicked");
1647	}
1648
1649	// ---- Startup Behavior ----
1650
1651	#[tokio::test]
1652	async fn startup_selects_earliest_group() {
1653		tokio::time::pause();
1654		let mut track = track_producer("test", hang::container::track_info());
1655		let consumer_track = track.subscribe(None);
1656		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
1657
1658		write_group(&mut track, 3, &[ts(0)]);
1659		write_group(&mut track, 5, &[ts(150_000)]);
1660
1661		let mut group7 = track.create_group(moq_net::group::Info { sequence: 7 }).unwrap();
1662		Container::Legacy
1663			.write(
1664				&mut group7,
1665				&[Frame {
1666					timestamp: ts(300_000),
1667					payload: Bytes::from_static(&[0xDE, 0xAD]),
1668					keyframe: false,
1669					duration: None,
1670				}],
1671			)
1672			.unwrap();
1673
1674		let finisher = tokio::spawn(async move {
1675			tokio::time::sleep(Duration::from_millis(50)).await;
1676			Container::Legacy
1677				.write(
1678					&mut group7,
1679					&[Frame {
1680						timestamp: ts(400_000),
1681						payload: Bytes::from_static(&[0xBE, 0xEF]),
1682						keyframe: false,
1683						duration: None,
1684					}],
1685				)
1686				.unwrap();
1687			group7.finish().unwrap();
1688			track.finish().unwrap();
1689		});
1690
1691		let _frames = tokio::time::timeout(Duration::from_secs(2), async {
1692			let mut frames = Vec::new();
1693			while let Some(frame) = consumer.read().await.unwrap() {
1694				frames.push(frame);
1695			}
1696			frames
1697		})
1698		.await
1699		.expect("should not hang");
1700
1701		finisher.await.unwrap();
1702	}
1703
1704	#[tokio::test]
1705	async fn startup_skips_groups_without_data() {
1706		tokio::time::pause();
1707		let mut track = track_producer("test", hang::container::track_info());
1708		let consumer_track = track.subscribe(None);
1709		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1710
1711		let _group5 = track.create_group(moq_net::group::Info { sequence: 5 }).unwrap();
1712		write_group(&mut track, 7, &[ts(210_000)]);
1713		track.finish().unwrap();
1714
1715		let frames = tokio::time::timeout(Duration::from_millis(500), async {
1716			let mut frames = Vec::new();
1717			while let Some(frame) = consumer.read().await.unwrap() {
1718				frames.push(frame);
1719			}
1720			frames
1721		})
1722		.await
1723		.expect("should not hang");
1724
1725		assert!(!frames.is_empty());
1726	}
1727
1728	#[tokio::test]
1729	async fn startup_single_group_mid_stream() {
1730		let mut track = track_producer("test", hang::container::track_info());
1731		let consumer_track = track.subscribe(None);
1732		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1733
1734		write_group(&mut track, 100, &[ts(3_000_000)]);
1735		track.finish().unwrap();
1736
1737		let frames = read_all(&mut consumer).await.unwrap();
1738		assert_eq!(frames.len(), 1);
1739	}
1740
1741	#[tokio::test]
1742	async fn multiple_sequential_latency_skips() {
1743		tokio::time::pause();
1744		let mut track = track_producer("test", hang::container::track_info());
1745		let consumer_track = track.subscribe(None);
1746		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(50));
1747
1748		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1749		Container::Legacy
1750			.write(
1751				&mut group0,
1752				&[Frame {
1753					timestamp: ts(0),
1754					payload: Bytes::from_static(&[0xAA]),
1755
1756					keyframe: false,
1757					duration: None,
1758				}],
1759			)
1760			.unwrap();
1761
1762		write_group(&mut track, 1, &[ts(100_000)]);
1763		write_group(&mut track, 2, &[ts(200_000)]);
1764		write_group(&mut track, 3, &[ts(300_000)]);
1765		track.finish().unwrap();
1766
1767		let finisher = tokio::spawn(async move {
1768			tokio::time::sleep(Duration::from_millis(20)).await;
1769			group0.finish().unwrap();
1770		});
1771
1772		let frames = read_all(&mut consumer).await.unwrap();
1773		assert!(!frames.is_empty());
1774		finisher.await.unwrap();
1775	}
1776
1777	#[tokio::test]
1778	async fn latency_skip_boundary_exact() {
1779		tokio::time::pause();
1780		let mut track = track_producer("test", hang::container::track_info());
1781		let consumer_track = track.subscribe(None);
1782		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
1783
1784		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1785		Container::Legacy
1786			.write(
1787				&mut group0,
1788				&[Frame {
1789					timestamp: ts(0),
1790					payload: Bytes::from_static(&[0xAA]),
1791
1792					keyframe: false,
1793					duration: None,
1794				}],
1795			)
1796			.unwrap();
1797
1798		write_group(&mut track, 1, &[ts(100_000)]);
1799		track.finish().unwrap();
1800
1801		let finisher = tokio::spawn(async move {
1802			tokio::time::sleep(Duration::from_millis(20)).await;
1803			group0.finish().unwrap();
1804		});
1805
1806		let frames = read_all(&mut consumer).await.unwrap();
1807		assert!(!frames.is_empty());
1808		finisher.await.unwrap();
1809	}
1810
1811	/// Regression: a single stalled group with one newer group should trigger
1812	/// a latency skip when the timestamp difference exceeds latency.
1813	/// Previously, the span was computed across newer groups only (zero for one
1814	/// group), so the skip never fired.
1815	#[tokio::test]
1816	async fn single_newer_group_triggers_skip() {
1817		tokio::time::pause();
1818		let mut track = track_producer("test", hang::container::track_info());
1819		let consumer_track = track.subscribe(None);
1820		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
1821
1822		// Group 0: stalled at ts=0, NOT finished
1823		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1824		Container::Legacy
1825			.write(
1826				&mut group0,
1827				&[Frame {
1828					timestamp: ts(0),
1829					payload: Bytes::from_static(&[0xDE, 0xAD]),
1830					keyframe: false,
1831					duration: None,
1832				}],
1833			)
1834			.unwrap();
1835
1836		// Group 1: finished, 200ms ahead (well beyond 100ms latency)
1837		write_group(&mut track, 1, &[ts(200_000)]);
1838		track.finish().unwrap();
1839
1840		let finisher = tokio::spawn(async move {
1841			tokio::time::sleep(Duration::from_millis(50)).await;
1842			group0.finish().unwrap();
1843		});
1844
1845		let frames = read_all(&mut consumer).await.unwrap();
1846		assert_eq!(frames.len(), 2, "Expected group 0 frame + group 1 frame");
1847		finisher.await.unwrap();
1848	}
1849
1850	/// Regression: when the current group is fully consumed and the next sequence
1851	/// is missing (gap), the consumer should skip to the next available group
1852	/// once the track is fully received, rather than hanging forever.
1853	#[tokio::test]
1854	async fn single_missing_sequence_near_eof_skips() {
1855		tokio::time::pause();
1856		let mut track = track_producer("test", hang::container::track_info());
1857		let consumer_track = track.subscribe(None);
1858		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
1859
1860		// Group 0: finished normally
1861		write_group(&mut track, 0, &[ts(0), ts(20_000)]);
1862		// Group 2: finished (group 1 is missing — sequence gap)
1863		write_group(&mut track, 2, &[ts(200_000)]);
1864		track.finish().unwrap();
1865
1866		let frames = read_all(&mut consumer).await.unwrap();
1867		assert_eq!(frames.len(), 3, "Expected group 0 (2 frames) + group 2 (1 frame)");
1868	}
1869
1870	#[tokio::test]
1871	async fn group_error_skips_to_next() {
1872		let mut track = track_producer("test", hang::container::track_info());
1873		let consumer_track = track.subscribe(None);
1874		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1875
1876		let group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1877		group0.abort(moq_net::Error::Cancel).unwrap();
1878
1879		write_group(&mut track, 1, &[ts(30_000)]);
1880		track.finish().unwrap();
1881
1882		let frames = read_all(&mut consumer).await.unwrap();
1883		assert_eq!(frames.len(), 1);
1884	}
1885
1886	#[tokio::test]
1887	async fn track_finishes_while_reading() {
1888		tokio::time::pause();
1889		let mut track = track_producer("test", hang::container::track_info());
1890		let consumer_track = track.subscribe(None);
1891		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1892
1893		write_group(&mut track, 0, &[ts(0)]);
1894
1895		let finisher = tokio::spawn(async move {
1896			tokio::time::sleep(Duration::from_millis(20)).await;
1897			write_group(&mut track, 1, &[ts(30_000)]);
1898			tokio::time::sleep(Duration::from_millis(20)).await;
1899			track.finish().unwrap();
1900		});
1901
1902		let frames = tokio::time::timeout(Duration::from_secs(2), async {
1903			let mut frames = Vec::new();
1904			while let Some(frame) = consumer.read().await.unwrap() {
1905				frames.push(frame);
1906			}
1907			frames
1908		})
1909		.await
1910		.expect("consumer should not hang");
1911
1912		assert_eq!(frames.len(), 2);
1913		finisher.await.unwrap();
1914	}
1915
1916	#[tokio::test]
1917	async fn empty_group_advances() {
1918		let mut track = track_producer("test", hang::container::track_info());
1919		let consumer_track = track.subscribe(None);
1920		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1921
1922		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1923		group0.finish().unwrap();
1924
1925		write_group(&mut track, 1, &[ts(30_000)]);
1926		track.finish().unwrap();
1927
1928		let frames = read_all(&mut consumer).await.unwrap();
1929		assert_eq!(frames.len(), 1);
1930	}
1931
1932	// ---- VideoConfig Container ----
1933
1934	#[tokio::test]
1935	async fn video_container_legacy() {
1936		tokio::time::pause();
1937
1938		let mut track = track_producer("video", hang::container::track_info());
1939		let consumer_track = track.subscribe(None);
1940		let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
1941
1942		// Write frames using Container::Legacy encoding
1943		let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1944		for i in 0..3u64 {
1945			let frame = Frame {
1946				timestamp: ts(i * 33_333),
1947				payload: Bytes::from_static(&[0xDE, 0xAD]),
1948				keyframe: false,
1949				duration: None,
1950			};
1951			Container::Legacy.write(&mut group, &[frame]).unwrap();
1952		}
1953		group.finish().unwrap();
1954		track.finish().unwrap();
1955
1956		let mut frames = Vec::new();
1957		while let Some(frame) = consumer.read().await.unwrap() {
1958			frames.push(frame);
1959		}
1960
1961		assert_eq!(frames.len(), 3);
1962		assert_eq!(frames[0].timestamp, ts(0));
1963		assert!(frames[0].keyframe);
1964		assert_eq!(frames[1].timestamp, ts(33_333));
1965		assert!(!frames[1].keyframe);
1966		assert_eq!(frames[2].timestamp, ts(66_666));
1967		assert!(!frames[2].keyframe);
1968	}
1969
1970	// ---- Duration Skipping ----
1971
1972	/// A stalled group whose frame covers up to the next group's start is skipped
1973	/// immediately, even with a latency budget far larger than the gap. Without
1974	/// duration support the consumer would block on the unfinished group forever.
1975	#[tokio::test]
1976	async fn duration_skip_advances_to_next_group() {
1977		tokio::time::pause();
1978		// DurationWire is a test-only container that doesn't stamp moq_net frame
1979		// timestamps; leave the track untimed so model-layer validation matches.
1980		let mut track = track_producer("test", None);
1981		let consumer_track = track.subscribe(None);
1982		// Latency dwarfs the gap, so only duration coverage can trigger the skip.
1983		let mut consumer = Consumer::new(consumer_track, DurationWire).with_latency(Duration::from_secs(10));
1984
1985		// Group 0: one frame at ts=0 lasting 33ms, never finished.
1986		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
1987		write_duration_frame(&mut group0, ts(0), ts(33_000));
1988
1989		// Group 1: finished, starts exactly where group 0's frame ends.
1990		let mut group1 = track.create_group(moq_net::group::Info { sequence: 1 }).unwrap();
1991		write_duration_frame(&mut group1, ts(33_000), ts(33_000));
1992		group1.finish().unwrap();
1993
1994		track.finish().unwrap();
1995
1996		let frames = tokio::time::timeout(Duration::from_secs(2), async {
1997			let mut frames = Vec::new();
1998			while let Some(frame) = consumer.read().await.unwrap() {
1999				frames.push(frame);
2000			}
2001			frames
2002		})
2003		.await
2004		.expect("consumer hung — duration skip regression");
2005
2006		assert_eq!(frames.len(), 2);
2007		assert_eq!(frames[0].timestamp, ts(0));
2008		assert_eq!(frames[1].timestamp, ts(33_000));
2009
2010		// group0 is intentionally never finished.
2011		drop(group0);
2012	}
2013
2014	/// When the current group's frame ends before the next group begins, there is
2015	/// still a gap to cover, so we don't skip early: a late-arriving frame on the
2016	/// slow group is delivered rather than dropped.
2017	#[tokio::test]
2018	async fn duration_below_gap_does_not_skip() {
2019		tokio::time::pause();
2020		// DurationWire is untimed at the moq_net frame layer.
2021		let mut track = track_producer("test", None);
2022		let consumer_track = track.subscribe(None);
2023		let mut consumer = Consumer::new(consumer_track, DurationWire).with_latency(Duration::from_secs(10));
2024
2025		// Group 0: frame at ts=0 lasting only 10ms, far short of group 1 at 33ms.
2026		let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
2027		write_duration_frame(&mut group0, ts(0), ts(10_000));
2028
2029		// Group 1: finished at 33ms.
2030		let mut group1 = track.create_group(moq_net::group::Info { sequence: 1 }).unwrap();
2031		write_duration_frame(&mut group1, ts(33_000), ts(33_000));
2032		group1.finish().unwrap();
2033		track.finish().unwrap();
2034
2035		// A second frame lands on group 0 and finishes it after the consumer has
2036		// had a chance to (incorrectly) skip.
2037		let finisher = tokio::spawn(async move {
2038			tokio::time::sleep(Duration::from_millis(20)).await;
2039			write_duration_frame(&mut group0, ts(20_000), ts(10_000));
2040			group0.finish().unwrap();
2041		});
2042
2043		let frames = tokio::time::timeout(Duration::from_secs(2), async {
2044			let mut frames = Vec::new();
2045			while let Some(frame) = consumer.read().await.unwrap() {
2046				frames.push(frame);
2047			}
2048			frames
2049		})
2050		.await
2051		.expect("consumer hung");
2052
2053		// The slow group's late frame survives because nothing covered the gap.
2054		assert_eq!(frames.len(), 3);
2055		assert_eq!(frames[0].timestamp, ts(0));
2056		assert_eq!(frames[1].timestamp, ts(20_000));
2057		assert_eq!(frames[2].timestamp, ts(33_000));
2058		finisher.await.unwrap();
2059	}
2060}