use std::collections::VecDeque;
use std::task::{Poll, ready};
use moq_net::Timestamp;
use super::{Container, Frame, TimestampRewind};
pub(crate) enum Event {
Frame(Frame),
FrameEnd(Timestamp),
GroupEnd,
}
pub struct Consumer<F: Container> {
track: moq_net::track::Subscriber,
format: F,
current: u64,
pending: VecDeque<GroupBuffer>,
startup: bool,
max_age: std::time::Duration,
live_edge: Option<(u64, Timestamp)>,
group_edge: Option<Timestamp>,
presented_end: Option<std::time::Duration>,
discontinuity: u64,
end: Option<Timestamp>,
}
const CONTIGUITY_TOLERANCE: std::time::Duration = std::time::Duration::from_millis(1);
fn pts_contiguous(end: Option<std::time::Duration>, next_start: std::time::Duration) -> bool {
end.is_some_and(|end| next_start <= end.saturating_add(CONTIGUITY_TOLERANCE))
}
impl<F: Container> Consumer<F> {
pub fn new(track: moq_net::track::Subscriber, format: F) -> Self {
let subscription = track.subscription();
let start = subscription.start.map(|position| position.group);
let max_age = subscription.max_age.min(track.info().max_age);
Self {
track,
format,
current: start.unwrap_or(0),
pending: VecDeque::new(),
startup: start.is_none(),
max_age,
live_edge: None,
group_edge: None,
presented_end: None,
discontinuity: 0,
end: None,
}
}
pub fn discontinuity(&self) -> u64 {
self.discontinuity
}
pub fn end(&self) -> Option<Timestamp> {
self.end
}
pub async fn read(&mut self) -> Result<Option<Frame>, F::Error> {
kio::wait(|waiter| self.poll_read(waiter)).await
}
pub fn poll_read(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Frame>, F::Error>> {
loop {
match ready!(self.poll_event(waiter))? {
Some(Event::Frame(frame)) => return Poll::Ready(Ok(Some(frame))),
Some(Event::FrameEnd(end)) => {
if self.format.kind() == super::Kind::Audio {
self.end = Some(end);
}
}
Some(Event::GroupEnd) => continue,
None => return Poll::Ready(Ok(None)),
}
}
}
pub(crate) fn poll_event(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Event>, F::Error>> {
let finished = self.poll_read_finish(waiter)?.is_ready();
if self.startup {
let any_frame = self
.pending
.iter_mut()
.any(|group| matches!(group.poll_min_timestamp(waiter, &self.format), Poll::Ready(Ok(_))));
if any_frame {
self.current = self.pending.front().expect("a group has a frame").sequence;
self.startup = false;
}
}
let current = self.current;
self.pending
.retain_mut(|group| group.sequence <= current || !group.buffered.is_empty() || !group.poll_aborted(waiter));
'read: loop {
self.poll_malformed(waiter)?;
if let Some(group) = self.pending.front_mut()
&& group.sequence <= self.current
{
match group.poll_read(waiter, &self.format) {
Poll::Ready(Ok(Some(Event::Frame(frame)))) => {
let seq = group.group.sequence;
let ts = frame.timestamp;
if self.group_edge.is_some_and(|edge| ts.as_micros() < edge.as_micros()) {
return Poll::Ready(Err(TimestampRewind.into()));
}
if self.live_edge.is_none_or(|(_, high)| ts.as_micros() > high.as_micros()) {
self.live_edge = Some((seq, ts));
}
return Poll::Ready(Ok(Some(Event::Frame(frame))));
}
Poll::Ready(Ok(Some(Event::FrameEnd(end)))) => {
let seq = group.group.sequence;
if self
.live_edge
.is_none_or(|(_, high)| end.as_micros() > high.as_micros())
{
self.live_edge = Some((seq, end));
}
return Poll::Ready(Ok(Some(Event::FrameEnd(end))));
}
Poll::Ready(Ok(Some(event))) => return Poll::Ready(Ok(Some(event))),
Poll::Pending => {}
Poll::Ready(Err(e)) => {
if !group.poll_aborted(waiter) {
tracing::warn!(
track = self.track.name(),
group = group.group.sequence,
error = ?e,
"group payload failed to decode; ending the reader"
);
return Poll::Ready(Err(e));
}
tracing::warn!(
track = self.track.name(),
group = group.group.sequence,
error = ?e,
"current group evicted; skipping to next buffered group"
);
self.pending.pop_front();
self.current = self.pending.front().map_or(self.current + 1, |g| g.sequence);
continue 'read;
}
Poll::Ready(Ok(None)) => {
let marker = group.marker();
if let Some(end) = group.max_end {
self.presented_end = Some(end);
}
self.pending.pop_front();
self.current += 1;
self.note_group_edge();
if marker {
self.bump_playhead();
}
return Poll::Ready(Ok(Some(Event::GroupEnd)));
}
}
}
let current_end = if let Some(current) = self.pending.front_mut()
&& current.sequence <= self.current
{
match current.poll_min_timestamp(waiter, &self.format) {
Poll::Ready(Ok(_)) => current.max_end,
_ => None,
}
} else {
None
};
let mut next_group = None;
for (i, group) in self.pending.iter_mut().enumerate() {
if group.sequence <= self.current {
continue;
}
if let Poll::Ready(Ok(ts)) = group.poll_min_timestamp(waiter, &self.format) {
next_group = Some((i, std::time::Duration::from(ts)));
break;
}
}
let mut max_timestamp = std::time::Duration::ZERO;
for group in self.pending.iter_mut().rev() {
if group.sequence <= self.current {
break;
}
if let Poll::Ready(Ok(ts)) = group.poll_max_timestamp(waiter, &self.format) {
max_timestamp = max_timestamp.max(ts.into());
break; }
}
if let Some(front_sequence) = self.pending.front().map(|g| g.sequence)
&& front_sequence > self.current
&& let Some((_, next_start)) = next_group
&& (finished || max_timestamp.saturating_sub(next_start) >= self.max_age)
{
if !pts_contiguous(current_end.or(self.presented_end), next_start) {
self.bump_playhead();
}
self.current = front_sequence;
self.note_group_edge();
continue;
}
let should_skip = next_group.is_some_and(|(_, next_start)| {
max_timestamp.saturating_sub(next_start) >= self.max_age
|| current_end.is_some_and(|end| end >= next_start)
});
if let Some((new_idx, next_start)) = next_group
&& should_skip
{
let hole = !pts_contiguous(current_end.or(self.presented_end), next_start);
let had_marker = self.pending.iter().take(new_idx).any(GroupBuffer::marker);
self.pending.drain(0..new_idx);
if hole || had_marker {
self.bump_playhead();
}
let new_current = self.pending.front().map(|g| g.sequence).unwrap();
tracing::debug!(old = self.current, new = new_current, "skipping slow groups");
self.current = new_current;
self.note_group_edge();
continue;
}
if finished
&& let Some(front_sequence) = self.pending.front().map(|g| g.sequence)
&& front_sequence > self.current
{
let _ = self.pending.front_mut().unwrap().buffer_all(waiter, &self.format);
let next_start = self
.pending
.front()
.and_then(|group| group.min_timestamp.map(std::time::Duration::from).or(group.max_end));
if let Some(start) = next_start
&& !pts_contiguous(current_end.or(self.presented_end), start)
{
self.bump_playhead();
}
self.current = front_sequence;
self.note_group_edge();
continue;
}
if finished && self.pending.is_empty() {
return Poll::Ready(Ok(None));
}
return Poll::Pending;
}
}
fn bump_playhead(&mut self) {
self.discontinuity += 1;
self.end = None;
}
fn note_group_edge(&mut self) {
if let Some((_, ts)) = self.live_edge {
self.group_edge = Some(ts);
}
}
fn poll_read_finish(&mut self, waiter: &kio::Waiter) -> Poll<Result<(), F::Error>> {
loop {
let next = match ready!(self.track.poll_recv_group(waiter)) {
Ok(next) => next,
Err(err) => {
tracing::warn!(track = self.track.name(), error = ?err, "track failed; ending the reader");
return Poll::Ready(Err(err.into()));
}
};
let Some(group) = next else {
return Poll::Ready(Ok(()));
};
let reader = GroupBuffer::new(group);
let sequence = reader.group.sequence;
if sequence < self.current {
tracing::debug!(old = ?sequence, current = ?self.current, "skipping old group");
continue;
}
let idx = self
.pending
.partition_point(|g| g.group.sequence < reader.group.sequence);
self.pending.insert(idx, reader);
}
}
fn poll_malformed(&mut self, waiter: &kio::Waiter) -> Result<(), F::Error> {
let Some((prev_group, edge)) = self.live_edge else {
return Ok(());
};
for group in self.pending.iter_mut() {
if group.group.sequence <= prev_group {
continue;
}
if let Poll::Ready(Ok(min)) = group.poll_min_timestamp(waiter, &self.format)
&& min.as_micros() < edge.as_micros()
{
return Err(TimestampRewind.into());
}
}
Ok(())
}
pub fn set_max_age(&mut self, max_age: std::time::Duration) {
self.max_age = max_age.min(self.track.info().max_age);
let subscription = self.track.subscription().with_max_age(max_age);
let _ = self.track.update(subscription);
}
}
struct GroupBuffer {
group: moq_net::group::Consumer,
index: usize,
empty: bool,
media: bool,
buffered: VecDeque<Frame>,
markers: VecDeque<(usize, Timestamp)>,
delivered: usize,
min_timestamp: Option<Timestamp>,
max_timestamp: Option<Timestamp>,
max_end: Option<std::time::Duration>,
}
impl GroupBuffer {
fn new(group: moq_net::group::Consumer) -> Self {
Self {
group,
index: 0,
empty: true,
media: false,
buffered: VecDeque::new(),
markers: VecDeque::new(),
delivered: 0,
max_timestamp: None,
min_timestamp: None,
max_end: None,
}
}
fn poll_read<F: Container>(&mut self, waiter: &kio::Waiter, format: &F) -> Poll<Result<Option<Event>, F::Error>> {
loop {
if self.markers.front().is_some_and(|(index, _)| *index <= self.delivered) {
let (_, end) = self.markers.pop_front().unwrap();
return Poll::Ready(Ok(Some(Event::FrameEnd(end))));
}
if let Some(frame) = self.buffered.pop_front() {
self.delivered += 1;
return Poll::Ready(Ok(Some(Event::Frame(frame))));
}
if !ready!(self.buffer_once(waiter, format)?) {
return Poll::Ready(Ok(None));
}
}
}
fn buffer_once<F: Container>(&mut self, waiter: &kio::Waiter, format: &F) -> Poll<Result<bool, F::Error>> {
let Some(frames) = ready!(format.poll_read(&mut self.group, waiter)?) else {
return Poll::Ready(Ok(false));
};
self.empty = false;
for mut frame in frames {
if let Some(bound) = format.end(&frame) {
self.note_end(bound);
self.markers.push_back((self.index, bound));
continue;
}
self.min_timestamp = Some(match self.min_timestamp {
Some(existing) => existing.min(frame.timestamp),
None => frame.timestamp,
});
self.max_timestamp = Some(match self.max_timestamp {
Some(existing) => existing.max(frame.timestamp),
None => frame.timestamp,
});
self.note_end(frame.timestamp);
if let Some(duration) = frame.duration {
let end = std::time::Duration::from(frame.timestamp) + std::time::Duration::from(duration);
self.max_end = Some(self.max_end.map_or(end, |existing| existing.max(end)));
}
frame.keyframe = frame.keyframe || self.index == 0;
self.index += 1;
self.media = true;
self.buffered.push_back(frame);
}
Poll::Ready(Ok(true))
}
fn buffer_one<F: Container>(&mut self, waiter: &kio::Waiter, format: &F) -> Poll<Result<bool, F::Error>> {
loop {
if !self.buffered.is_empty() {
return Poll::Ready(Ok(true));
}
if !ready!(self.buffer_once(waiter, format)?) {
return Poll::Ready(Ok(false));
}
}
}
fn buffer_all<F: Container>(&mut self, waiter: &kio::Waiter, format: &F) -> Poll<Result<(), F::Error>> {
while ready!(self.buffer_once(waiter, format)?) {}
Poll::Ready(Ok(()))
}
fn poll_max_timestamp<F: Container>(
&mut self,
waiter: &kio::Waiter,
format: &F,
) -> Poll<Result<Timestamp, F::Error>> {
let _ = self.buffer_all(waiter, format)?;
if let Some(max) = self.max_timestamp {
return Poll::Ready(Ok(max));
}
if let Poll::Ready(_frames) = self.group.poll_finished(waiter)? {
return Poll::Ready(Err(moq_net::Error::Decode(moq_net::DecodeError::Short).into()));
}
Poll::Pending
}
fn poll_min_timestamp<F: Container>(
&mut self,
waiter: &kio::Waiter,
format: &F,
) -> Poll<Result<Timestamp, F::Error>> {
let _ = self.buffer_one(waiter, format)?;
if let Some(min) = self.min_timestamp {
return Poll::Ready(Ok(min));
}
if let Poll::Ready(_frames) = self.group.poll_finished(waiter)? {
return Poll::Ready(Err(moq_net::Error::Decode(moq_net::DecodeError::Short).into()));
}
Poll::Pending
}
fn poll_aborted(&mut self, waiter: &kio::Waiter) -> bool {
matches!(self.group.poll_finished(waiter), Poll::Ready(Err(_)))
}
fn note_end(&mut self, timestamp: Timestamp) {
let end = std::time::Duration::from(timestamp);
self.max_end = Some(self.max_end.map_or(end, |existing| existing.max(end)));
}
fn marker(&self) -> bool {
!self.empty && !self.media
}
}
impl std::ops::Deref for GroupBuffer {
type Target = moq_net::group::Consumer;
fn deref(&self) -> &Self::Target {
&self.group
}
}
#[cfg(test)]
mod tests {
use super::Container as ContainerTrait;
use super::*;
use crate::catalog::hang::Container;
use std::time::Duration;
use bytes::Bytes;
fn track_producer(
name: impl Into<std::sync::Arc<str>>,
info: impl Into<Option<moq_net::track::Info>>,
) -> moq_net::track::Producer {
moq_net::broadcast::Info::new()
.produce()
.create_track(name, info)
.unwrap()
}
fn ts(micros: u64) -> Timestamp {
Timestamp::from_micros(micros).unwrap()
}
struct DurationWire;
fn encode_duration_frame(timestamp: Timestamp, duration: Timestamp) -> Vec<u8> {
let mut buf = Vec::with_capacity(18);
buf.extend_from_slice(&(timestamp.as_micros() as u64).to_le_bytes());
buf.extend_from_slice(&(duration.as_micros() as u64).to_le_bytes());
buf.extend_from_slice(&[0xDE, 0xAD]);
buf
}
impl ContainerTrait for DurationWire {
type Error = crate::Error;
fn write(&self, group: &mut moq_net::group::Producer, frames: &[Frame]) -> Result<(), Self::Error> {
for frame in frames {
group.write_frame(frame.timestamp, encode_duration_frame(frame.timestamp, ts(0)))?;
}
Ok(())
}
fn poll_read(
&self,
group: &mut moq_net::group::Consumer,
waiter: &kio::Waiter,
) -> Poll<Result<Option<Vec<Frame>>, Self::Error>> {
use bytes::Buf;
let Some(mut data) = ready!(group.poll_read_frame(waiter)?).map(|f| f.payload) else {
return Poll::Ready(Ok(None));
};
let timestamp = ts(data.get_u64_le());
let duration = ts(data.get_u64_le());
let payload = data.copy_to_bytes(data.remaining());
Poll::Ready(Ok(Some(vec![Frame {
timestamp,
payload,
keyframe: false,
duration: Some(duration),
}])))
}
}
fn write_duration_frame(group: &mut moq_net::group::Producer, timestamp: Timestamp, duration: Timestamp) {
group
.write_frame(timestamp, encode_duration_frame(timestamp, duration))
.unwrap();
}
fn write_group(track: &mut moq_net::track::Producer, sequence: u64, timestamps: &[Timestamp]) {
let mut group = track.create_group(moq_net::group::Info { sequence }).unwrap();
for ×tamp in timestamps {
let frame = Frame {
timestamp,
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
};
Container::Legacy(crate::container::Kind::Data)
.write(&mut group, &[frame])
.unwrap();
}
group.finish().unwrap();
}
async fn read_all(consumer: &mut Consumer<Container>) -> Result<Vec<Frame>, crate::Error> {
let mut frames = Vec::new();
loop {
match tokio::time::timeout(Duration::from_millis(200), consumer.read()).await {
Ok(Ok(Some(frame))) => frames.push(frame),
Ok(Ok(None)) => break,
Ok(Err(e)) => return Err(e),
Err(_) => panic!(
"read_all: Consumer::read timed out after 200ms ({} frames collected so far)",
frames.len()
),
}
}
Ok(frames)
}
fn container_max_age_only(track: moq_net::track::Subscriber, max_age: std::time::Duration) -> Consumer<Container> {
let control = track.control();
let mut consumer = Consumer::new(track, Container::Legacy(crate::container::Kind::Data));
consumer.set_max_age(max_age);
control
.update(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(30)))
.unwrap();
consumer
}
#[test]
fn new_inherits_the_initial_subscription_latency() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let max_age = Duration::from_millis(250);
let subscriber = track.subscribe(moq_net::track::Subscription::default().with_max_age(max_age));
let consumer = Consumer::new(subscriber, Container::Legacy(crate::container::Kind::Data));
assert_eq!(consumer.max_age, max_age);
assert_eq!(consumer.track.subscription().max_age, max_age);
}
#[tokio::test]
async fn read_single_group() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].timestamp, ts(0));
assert!(frames[0].keyframe);
assert!(consumer.read().await.unwrap().is_none());
}
fn write_marker_group(track: &mut moq_net::track::Producer, sequence: u64, timestamp: Timestamp) {
let mut group = track.create_group(moq_net::group::Info { sequence }).unwrap();
Container::Legacy(crate::container::Kind::Audio)
.write(
&mut group,
&[Frame {
timestamp,
payload: Bytes::new(),
keyframe: false,
duration: None,
}],
)
.unwrap();
group.finish().unwrap();
}
#[tokio::test]
async fn empty_group_declares_a_discontinuity() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.audio));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(2)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Audio));
write_group(&mut track, 0, &[ts(0)]);
write_marker_group(&mut track, 1, ts(0));
write_group(&mut track, 2, &[ts(1_000_000)]);
track.finish().unwrap();
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(0));
assert_eq!(consumer.discontinuity(), 0);
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(1_000_000));
assert_eq!(consumer.discontinuity(), 1);
}
#[tokio::test]
async fn empty_groups_mean_nothing() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.audio));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(2)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Audio));
write_group(&mut track, 0, &[ts(0)]);
track
.create_group(moq_net::group::Info { sequence: 1 })
.unwrap()
.finish()
.unwrap();
write_group(&mut track, 2, &[ts(1_000)]);
track.finish().unwrap();
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(0));
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(1_000));
assert_eq!(consumer.discontinuity(), 0, "an empty group is not a marker");
}
#[tokio::test]
async fn latency_skip_preserves_empty_group_discontinuity() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.audio));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(2)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Audio));
consumer.max_age = Duration::ZERO;
write_group(&mut track, 0, &[ts(0)]);
write_group(&mut track, 3, &[ts(1_000_000)]);
track.finish().unwrap();
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(0));
assert_eq!(consumer.discontinuity(), 0);
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(1_000_000));
assert_eq!(
consumer.discontinuity(),
1,
"a shed marker is a timestamp hole, so the playhead jumps"
);
}
#[tokio::test]
async fn read_multiple_frames_single_group() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0), ts(33_000), ts(66_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 3);
assert_eq!(frames[0].timestamp, ts(0));
assert_eq!(frames[1].timestamp, ts(33_000));
assert_eq!(frames[2].timestamp, ts(66_000));
assert!(frames[0].keyframe);
}
#[tokio::test]
async fn read_multiple_groups_within_latency() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
for i in 0..5u64 {
write_group(&mut track, i, &[ts(i * 20_000)]);
}
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 5);
}
#[tokio::test]
async fn latency_skip_delivers_recent_groups() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(100)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
for f in 0..5u64 {
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp: ts(f * 2_000),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
}
for g in 1..20u64 {
let timestamps: Vec<_> = (0..5).map(|f| ts(g * 15_000 + f * 2_000)).collect();
write_group(&mut track, g, ×tamps);
}
track.finish().unwrap();
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
group0.finish().unwrap();
});
let frames = read_all(&mut consumer).await.unwrap();
assert!(frames.len() >= 25, "Expected >= 25 frames, got {}", frames.len());
finisher.await.expect("finisher task panicked");
}
#[tokio::test]
async fn zero_latency_skips_aggressively() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track = track.subscribe(None);
let mut consumer = container_max_age_only(consumer_track, Duration::ZERO);
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
for g in 1..10u64 {
let timestamps: Vec<_> = (0..3).map(|f| ts(g * 50_000 + f * 5_000)).collect();
write_group(&mut track, g, ×tamps);
}
track.finish().unwrap();
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
group0.finish().unwrap();
});
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 28, "Expected group 0 frame + groups 1-9");
assert!(!frames.is_empty(), "Expected at least some frames");
finisher.await.expect("finisher task panicked");
}
#[tokio::test]
async fn latency_skip_correctness() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track = track.subscribe(None);
let mut consumer = container_max_age_only(consumer_track, Duration::from_millis(100));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
for g in 1..10u64 {
write_group(&mut track, g, &[ts(g * 30_000)]);
}
track.finish().unwrap();
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
group0.finish().unwrap();
});
let frames = read_all(&mut consumer).await.unwrap();
assert!(!frames.is_empty(), "Expected at least some frames");
assert_eq!(frames.len(), 10, "Expected group 0 frame + groups 1-9");
assert_eq!(frames[0].timestamp, ts(0));
for i in 1..10u64 {
assert_eq!(frames[i as usize].timestamp, ts(i * 30_000));
}
finisher.await.expect("finisher task panicked");
}
#[test]
fn a_long_group_blocked_behind_a_newer_one_is_not_cut_short() {
let waiter = kio::Waiter::noop();
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut consumer = container_max_age_only(track.subscribe(None), Duration::from_secs(2));
let write = |group: &mut moq_net::group::Producer, timestamp: Timestamp| {
let frame = Frame {
timestamp,
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
};
Container::Legacy(crate::container::Kind::Data)
.write(group, &[frame])
.unwrap();
};
let read = |consumer: &mut Consumer<Container>| match consumer.poll_read(&waiter) {
Poll::Ready(Ok(Some(frame))) => Some(frame.timestamp),
Poll::Pending => None,
other => panic!(
"unexpected read: {:?}",
other.map(|r| r.map(|f| f.map(|f| f.timestamp)))
),
};
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
write(&mut group0, ts(0));
write(&mut group0, ts(500_000));
let mut group1 = track.create_group(moq_net::group::Info { sequence: 1 }).unwrap();
write(&mut group1, ts(2_500_000));
write(&mut group1, ts(3_000_000));
assert_eq!(read(&mut consumer), Some(ts(0)));
assert_eq!(read(&mut consumer), Some(ts(500_000)));
assert_eq!(read(&mut consumer), None, "cut group 0 short");
write(&mut group0, ts(1_000_000));
assert_eq!(read(&mut consumer), Some(ts(1_000_000)));
group0.finish().unwrap();
assert_eq!(read(&mut consumer), Some(ts(2_500_000)));
let mut group2 = track.create_group(moq_net::group::Info { sequence: 2 }).unwrap();
write(&mut group2, ts(5_000_000));
write(&mut group2, ts(7_000_000));
assert_eq!(read(&mut consumer), Some(ts(3_000_000)));
assert_eq!(read(&mut consumer), Some(ts(5_000_000)));
group1.finish().unwrap();
group2.finish().unwrap();
}
#[tokio::test]
async fn a_group_below_the_live_edge_aborts() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut consumer = Consumer::new(track.subscribe(None), Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(100_000)]);
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(100_000));
write_group(&mut track, 1, &[ts(0)]);
track.finish().unwrap();
let err = consumer.read().await.unwrap_err();
assert!(matches!(err, crate::Error::TimestampRewind(_)));
}
#[tokio::test]
async fn b_frames_within_a_group_are_accepted() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut consumer = Consumer::new(track.subscribe(None), Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0), ts(66_000), ts(33_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(
frames.iter().map(|f| f.timestamp).collect::<Vec<_>>(),
vec![ts(0), ts(66_000), ts(33_000)]
);
assert_eq!(consumer.discontinuity(), 0);
}
#[tokio::test]
async fn open_gop_leading_pictures_above_the_previous_group_are_accepted() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut consumer = Consumer::new(track.subscribe(None), Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0), ts(33_000)]);
write_group(&mut track, 1, &[ts(66_000), ts(50_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(
frames.iter().map(|f| f.timestamp.as_micros()).collect::<Vec<_>>(),
vec![0, 33_000, 66_000, 50_000]
);
assert_eq!(consumer.discontinuity(), 0);
}
#[tokio::test]
async fn a_marker_group_and_forward_jump_bumps_playhead_once() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.audio));
let mut consumer = Consumer::new(
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(2))),
Container::Legacy(crate::container::Kind::Audio),
);
write_group(&mut track, 0, &[ts(0)]);
write_marker_group(&mut track, 1, ts(0));
write_group(&mut track, 2, &[ts(1_000_000)]);
track.finish().unwrap();
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(0));
assert_eq!(consumer.discontinuity(), 0);
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(1_000_000));
assert_eq!(consumer.discontinuity(), 1);
}
#[tokio::test]
async fn a_zero_budget_idle_resume_jumps_the_playhead_after_a_shed_marker() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.audio));
let mut consumer = Consumer::new(track.subscribe(None), Container::Legacy(crate::container::Kind::Audio));
consumer.max_age = Duration::ZERO;
write_group(&mut track, 0, &[ts(0)]);
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(0));
write_marker_group(&mut track, 1, ts(0));
write_group(&mut track, 2, &[ts(1_000_000)]);
track.finish().unwrap();
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(1_000_000));
assert_eq!(
consumer.discontinuity(),
1,
"a shed marker with a timestamp jump is a playhead event"
);
}
#[tokio::test]
async fn a_zero_budget_skip_keeps_a_contiguous_marker() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.audio));
let mut consumer = Consumer::new(track.subscribe(None), Container::Legacy(crate::container::Kind::Audio));
consumer.max_age = Duration::ZERO;
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
Container::Legacy(crate::container::Kind::Audio)
.write(
&mut group0,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(0));
write_marker_group(&mut track, 1, ts(10_000));
write_group(&mut track, 2, &[ts(10_000)]);
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(10_000));
assert_eq!(
consumer.discontinuity(),
1,
"a drained marker still declares the encoder restart"
);
group0.finish().unwrap();
}
#[tokio::test]
async fn a_later_frame_below_the_previous_group_edge_aborts() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut consumer = Consumer::new(track.subscribe(None), Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(100_000)]);
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(100_000));
write_group(&mut track, 1, &[ts(200_000), ts(50_000)]);
track.finish().unwrap();
assert_eq!(consumer.read().await.unwrap().unwrap().timestamp, ts(200_000));
let err = consumer.read().await.unwrap_err();
assert!(matches!(err, crate::Error::TimestampRewind(_)));
}
fn write_marker(group: &mut moq_net::group::Producer, timestamp: Timestamp) {
let frame = Frame {
timestamp,
payload: Bytes::new(),
keyframe: false,
duration: None,
};
Container::Legacy(crate::container::Kind::Data)
.write(group, &[frame])
.unwrap();
}
#[tokio::test]
async fn empty_payload_is_skipped() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Video));
let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
let media = |timestamp| Frame {
timestamp,
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
};
Container::Legacy(crate::container::Kind::Video)
.write(&mut group, &[media(ts(0))])
.unwrap();
write_marker(&mut group, ts(16_000)); Container::Legacy(crate::container::Kind::Video)
.write(&mut group, &[media(ts(33_000))])
.unwrap();
write_marker(&mut group, ts(50_000)); group.finish().unwrap();
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 2, "markers are not surfaced as media");
assert_eq!(frames[0].timestamp, ts(0));
assert_eq!(frames[1].timestamp, ts(33_000));
}
#[tokio::test]
async fn leading_marker_preserves_the_first_media_keyframe() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Video));
let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
write_marker(&mut group, ts(20_000));
Container::Legacy(crate::container::Kind::Video)
.write(
&mut group,
&[Frame {
timestamp: ts(20_000),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
group.finish().unwrap();
track.finish().unwrap();
let frame = consumer.read().await.unwrap().unwrap();
assert!(frame.keyframe, "the marker does not consume the first-media slot");
assert!(frame.duration.is_none(), "a leading marker has no previous frame");
}
#[tokio::test]
async fn consecutive_markers_do_not_stall() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Video));
let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
Container::Legacy(crate::container::Kind::Video)
.write(
&mut group,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
for i in 1..5u64 {
write_marker(&mut group, ts(i * 1_000));
}
group.finish().unwrap();
write_group(&mut track, 1, &[ts(100_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
assert_eq!(micros, vec![0, 100_000], "markers skipped, next group reached");
}
#[tokio::test]
async fn loc_empty_payload_is_skipped() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Loc(crate::container::Kind::Video));
let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
let media = Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: true,
duration: None,
};
Container::Loc(crate::container::Kind::Video)
.write(&mut group, &[media])
.unwrap();
Container::Loc(crate::container::Kind::Video)
.write(
&mut group,
&[Frame {
timestamp: ts(33_000),
payload: Bytes::new(),
keyframe: false,
duration: None,
}],
)
.unwrap();
group.finish().unwrap();
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 1, "the empty LOC payload is not submitted");
assert_eq!(frames[0].timestamp, ts(0));
}
#[tokio::test]
async fn groups_delivered_in_sequence_order() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
write_group(&mut track, 2, &[ts(60_000)]);
write_group(&mut track, 1, &[ts(30_000)]);
track.finish().unwrap();
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(10)).await;
group0.finish().unwrap();
});
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 3);
assert_eq!(frames[0].timestamp, ts(0));
assert_eq!(frames[1].timestamp, ts(30_000));
assert_eq!(frames[2].timestamp, ts(60_000));
finisher.await.expect("finisher task panicked");
}
#[tokio::test]
async fn adjacent_group_flushed_immediately() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0)]);
write_group(&mut track, 1, &[ts(30_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].timestamp, ts(0));
assert_eq!(frames[1].timestamp, ts(30_000));
}
#[tokio::test]
async fn bframes_within_group() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0), ts(66_000), ts(33_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 3);
assert_eq!(frames[0].timestamp, ts(0));
assert_eq!(frames[1].timestamp, ts(66_000));
assert_eq!(frames[2].timestamp, ts(33_000));
}
#[tokio::test]
async fn empty_track_returns_none() {
tokio::time::pause();
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
track.finish().unwrap();
let result = tokio::time::timeout(Duration::from_millis(200), consumer.read()).await;
match result {
Ok(Ok(None)) => {} Ok(Ok(Some(_))) => panic!("expected None for empty track, got Some"),
Ok(Err(e)) => panic!("expected None for empty track, got error: {e}"),
Err(_) => panic!("should not hang on empty track"),
}
}
#[tokio::test]
async fn track_closed_with_error() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0)]);
track.abort(moq_net::Error::Cancel).unwrap();
let result = tokio::time::timeout(Duration::from_millis(500), async {
let mut frames = Vec::new();
while let Ok(Some(frame)) = consumer.read().await {
frames.push(frame);
}
frames
})
.await;
assert!(result.is_ok(), "Consumer should not hang after track error");
}
#[tokio::test]
async fn gap_in_group_sequence_recovery() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(100)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0), ts(20_000)]);
write_group(&mut track, 1, &[ts(40_000), ts(60_000)]);
write_group(&mut track, 3, &[ts(120_000), ts(140_000)]);
write_group(&mut track, 4, &[ts(160_000), ts(180_000)]);
write_group(&mut track, 5, &[ts(200_000), ts(220_000)]);
write_group(&mut track, 6, &[ts(240_000), ts(260_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert!(frames.len() >= 4, "Expected >= 4 frames, got {}", frames.len());
}
#[tokio::test]
async fn gap_at_start_of_sequence() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(80)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 5, &[ts(0), ts(20_000)]);
write_group(&mut track, 7, &[ts(80_000), ts(100_000)]);
write_group(&mut track, 8, &[ts(120_000), ts(140_000)]);
write_group(&mut track, 9, &[ts(160_000), ts(180_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert!(frames.len() >= 4, "Expected >= 4 frames, got {}", frames.len());
}
#[tokio::test]
async fn evicted_group_with_gap_skips_to_live() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(100)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
let first = consumer.read().await.unwrap().unwrap();
assert_eq!(first.timestamp, ts(0));
write_group(&mut track, 5, &[ts(150_000)]);
group0.abort(moq_net::Error::Old).unwrap();
let next = tokio::time::timeout(Duration::from_secs(1), consumer.read())
.await
.expect("consumer hung on an evicted group / gap")
.unwrap()
.unwrap();
assert_eq!(next.timestamp, ts(150_000), "skipped the evicted gap to the live group");
}
#[tokio::test]
async fn missing_sequence_skips_on_live_track() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(100)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0), ts(20_000)]);
write_group(&mut track, 2, &[ts(200_000)]);
write_group(&mut track, 3, &[ts(320_000)]);
let reached = tokio::time::timeout(Duration::from_secs(1), async {
loop {
let frame = consumer.read().await.unwrap().unwrap();
if frame.timestamp == ts(200_000) {
return;
}
}
})
.await;
assert!(reached.is_ok(), "consumer hung on a missing sequence on a live track");
}
#[tokio::test]
async fn gap_keeps_late_arriving_group_within_max_age() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0)]);
write_group(&mut track, 2, &[ts(200_000)]);
let first = consumer.read().await.unwrap().expect("group 0 frame");
assert_eq!(first.timestamp, ts(0));
assert!(
tokio::time::timeout(Duration::from_millis(50), consumer.read())
.await
.is_err(),
"the gap at sequence 1 is within budget and must be waited for"
);
write_group(&mut track, 1, &[ts(100_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
assert_eq!(micros, vec![100_000, 200_000], "the late group is delivered in order");
}
#[tokio::test]
async fn startup_keeps_late_arriving_head_group_within_max_age() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track = track.subscribe(
moq_net::track::Subscription::default()
.with_start(moq_net::track::Position::group(0))
.with_max_age(Duration::from_millis(500)),
);
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 1, &[ts(100_000)]);
assert!(
tokio::time::timeout(Duration::from_millis(50), consumer.read())
.await
.is_err(),
"the requested head is within budget and must be waited for"
);
write_group(&mut track, 0, &[ts(0)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
assert_eq!(micros, vec![0, 100_000], "the head group is delivered first");
}
#[tokio::test]
async fn startup_keeps_slow_earlier_stream_within_max_age() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
write_group(&mut track, 1, &[ts(100_000)]);
assert!(
tokio::time::timeout(Duration::from_millis(50), consumer.read())
.await
.is_err(),
"group 0's frames are within budget and must be waited for"
);
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
group0.finish().unwrap();
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
assert_eq!(micros, vec![0, 100_000], "the slow stream's frames are delivered");
}
struct FailingDecode;
impl ContainerTrait for FailingDecode {
type Error = crate::Error;
fn write(&self, group: &mut moq_net::group::Producer, frames: &[Frame]) -> Result<(), Self::Error> {
for frame in frames {
group.write_frame(moq_net::Timestamp::ZERO, frame.payload.clone())?;
}
Ok(())
}
fn poll_read(
&self,
group: &mut moq_net::group::Consumer,
waiter: &kio::Waiter,
) -> Poll<Result<Option<Vec<Frame>>, Self::Error>> {
use bytes::Buf;
let Some(mut data) = ready!(group.poll_read_frame(waiter)?).map(|f| f.payload) else {
return Poll::Ready(Ok(None));
};
if data.as_ref() == b"FAIL" {
return Poll::Ready(Err(crate::Error::UnknownFormat("malformed payload".into())));
}
Poll::Ready(Ok(Some(vec![Frame {
timestamp: ts(data.get_u64_le()),
payload: Bytes::new(),
keyframe: false,
duration: None,
}])))
}
}
#[tokio::test]
async fn decode_error_propagates() {
tokio::time::pause();
let track = track_producer("test", None);
let consumer_track = track.subscribe(None);
let mut consumer = Consumer::new(consumer_track, FailingDecode);
let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
group
.write_frame(moq_net::Timestamp::ZERO, Bytes::from(0u64.to_le_bytes().to_vec()))
.unwrap();
group
.write_frame(moq_net::Timestamp::ZERO, Bytes::from_static(b"FAIL"))
.unwrap();
group.finish().unwrap();
track.finish().unwrap();
let first = consumer.read().await;
assert!(matches!(first, Ok(Some(_))), "first frame should decode, got {first:?}");
let second = tokio::time::timeout(Duration::from_millis(200), consumer.read())
.await
.expect("consumer hung on a decode error");
assert!(
matches!(second, Err(crate::Error::UnknownFormat(_))),
"decode error must propagate, got {second:?}"
);
}
#[tokio::test]
async fn frame_timestamp_and_index_decoding() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0), ts(33_333), ts(66_666)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 3);
assert_eq!(frames[0].timestamp, ts(0));
assert!(frames[0].keyframe);
assert_eq!(frames[1].timestamp, ts(33_333));
assert_eq!(frames[2].timestamp, ts(66_666));
}
#[tokio::test]
async fn frame_payload_preserved() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let payload_bytes = vec![0x01, 0x02, 0x03, 0x04, 0x05];
let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group,
&[Frame {
timestamp: ts(0),
payload: Bytes::from(payload_bytes.clone()),
keyframe: false,
duration: None,
}],
)
.unwrap();
group.finish().unwrap();
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 1);
use bytes::Buf;
let mut received = Vec::new();
let mut payload = frames[0].payload.clone();
while payload.has_remaining() {
received.push(payload.get_u8());
}
assert_eq!(received, payload_bytes);
}
#[tokio::test]
async fn no_infinite_loop_with_buffered_frames() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(10)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
write_group(&mut track, 1, &[ts(100_000)]);
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(20)).await;
write_group(&mut track, 2, &[ts(200_000)]);
tokio::time::sleep(Duration::from_millis(20)).await;
group0.finish().unwrap();
track.finish().unwrap();
});
let frames = tokio::time::timeout(Duration::from_secs(2), async {
let mut frames = Vec::new();
while let Some(frame) = consumer.read().await.unwrap() {
frames.push(frame);
}
frames
})
.await
.expect("consumer hung — possible infinite loop regression");
assert_eq!(frames.len(), 3);
finisher.await.expect("finisher task panicked");
}
#[tokio::test]
async fn large_timestamps() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(3700)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let one_hour = 3_600_000_000u64;
write_group(&mut track, 0, &[ts(one_hour)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].timestamp, ts(one_hour));
assert_eq!(frames[0].timestamp.as_micros(), one_hour as u128);
}
#[tokio::test]
async fn set_max_age_changes_behavior() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(10)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0)]);
track.finish().unwrap();
let frame = consumer.read().await.unwrap().unwrap();
assert_eq!(frame.timestamp, ts(0));
consumer.set_max_age(Duration::from_millis(100));
assert!(consumer.read().await.unwrap().is_none());
}
#[tokio::test]
async fn max_timestamp_tracks_through_bframes() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(110)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
for ×tamp in &[ts(0), ts(66_000), ts(33_000)] {
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp,
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
}
write_group(&mut track, 1, &[ts(100_000)]);
track.finish().unwrap();
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
group0.finish().unwrap();
});
let frames = tokio::time::timeout(Duration::from_secs(2), async {
let mut frames = Vec::new();
while let Some(frame) = consumer.read().await.unwrap() {
frames.push(frame);
}
frames
})
.await
.expect("consumer hung — max_timestamp regression");
assert_eq!(frames.len(), 4, "Expected all 4 frames, got {}", frames.len());
assert_eq!(frames[0].timestamp, ts(0));
assert_eq!(frames[1].timestamp, ts(66_000));
assert_eq!(frames[2].timestamp, ts(33_000));
assert_eq!(frames[3].timestamp, ts(100_000));
finisher.await.expect("finisher task panicked");
}
#[tokio::test]
async fn startup_selects_earliest_group() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(100)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 3, &[ts(0)]);
write_group(&mut track, 5, &[ts(150_000)]);
let mut group7 = track.create_group(moq_net::group::Info { sequence: 7 }).unwrap();
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group7,
&[Frame {
timestamp: ts(300_000),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group7,
&[Frame {
timestamp: ts(400_000),
payload: Bytes::from_static(&[0xBE, 0xEF]),
keyframe: false,
duration: None,
}],
)
.unwrap();
group7.finish().unwrap();
track.finish().unwrap();
});
let _frames = tokio::time::timeout(Duration::from_secs(2), async {
let mut frames = Vec::new();
while let Some(frame) = consumer.read().await.unwrap() {
frames.push(frame);
}
frames
})
.await
.expect("should not hang");
finisher.await.unwrap();
}
#[tokio::test]
async fn startup_skips_groups_without_data() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let _group5 = track.create_group(moq_net::group::Info { sequence: 5 }).unwrap();
write_group(&mut track, 7, &[ts(210_000)]);
assert!(
tokio::time::timeout(Duration::from_millis(50), consumer.read())
.await
.is_err(),
"an open frameless group within budget must be waited for"
);
write_group(&mut track, 9, &[ts(800_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
assert_eq!(micros, vec![210_000, 800_000], "the expired frameless group is skipped");
}
#[tokio::test]
async fn aborted_frameless_group_after_a_gap_ends_the_track() {
tokio::time::pause();
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let group1 = track.create_group(moq_net::group::Info { sequence: 1 }).unwrap();
track.finish().unwrap();
assert!(
tokio::time::timeout(Duration::from_millis(50), consumer.read())
.await
.is_err(),
"an open frameless group must be waited for"
);
group1.abort(moq_net::Error::Old).unwrap();
let end = tokio::time::timeout(Duration::from_millis(200), consumer.read())
.await
.expect("an aborted frameless group must not park the consumer");
assert!(end.unwrap().is_none(), "the track ends cleanly");
}
#[tokio::test]
async fn finished_track_waits_for_an_open_head_group() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
write_group(&mut track, 1, &[ts(100_000)]);
track.finish().unwrap();
assert!(
tokio::time::timeout(Duration::from_millis(50), consumer.read())
.await
.is_err(),
"the open head group is within budget and must be waited for"
);
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
group0.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
assert_eq!(micros, vec![0, 100_000], "the open group's late frames are delivered");
}
#[tokio::test]
async fn startup_single_group_mid_stream() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 100, &[ts(3_000_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 1);
}
#[tokio::test]
async fn startup_mid_stream_live_track_starts_at_the_served_group() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 100, &[ts(3_000_000)]);
let frame = tokio::time::timeout(Duration::from_millis(200), consumer.read())
.await
.expect("an unfloored join must not wait out a gap below the served start")
.unwrap()
.expect("track still live");
assert_eq!(frame.timestamp, ts(3_000_000));
}
#[tokio::test]
async fn multiple_sequential_latency_skips() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(50)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xAA]),
keyframe: false,
duration: None,
}],
)
.unwrap();
write_group(&mut track, 1, &[ts(100_000)]);
write_group(&mut track, 2, &[ts(200_000)]);
write_group(&mut track, 3, &[ts(300_000)]);
track.finish().unwrap();
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(20)).await;
group0.finish().unwrap();
});
let frames = read_all(&mut consumer).await.unwrap();
assert!(!frames.is_empty());
finisher.await.unwrap();
}
#[tokio::test]
async fn latency_skip_boundary_exact() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(100)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xAA]),
keyframe: false,
duration: None,
}],
)
.unwrap();
write_group(&mut track, 1, &[ts(100_000)]);
track.finish().unwrap();
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(20)).await;
group0.finish().unwrap();
});
let frames = read_all(&mut consumer).await.unwrap();
assert!(!frames.is_empty());
finisher.await.unwrap();
}
#[tokio::test]
async fn single_newer_group_triggers_skip() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track = track.subscribe(None);
let mut consumer = container_max_age_only(consumer_track, Duration::from_millis(100));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
Container::Legacy(crate::container::Kind::Data)
.write(
&mut group0,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
}],
)
.unwrap();
write_group(&mut track, 1, &[ts(200_000)]);
track.finish().unwrap();
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
group0.finish().unwrap();
});
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 2, "Expected group 0 frame + group 1 frame");
finisher.await.unwrap();
}
#[tokio::test]
async fn single_missing_sequence_near_eof_skips() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track = track.subscribe(None);
let mut consumer = container_max_age_only(consumer_track, Duration::from_millis(100));
write_group(&mut track, 0, &[ts(0), ts(20_000)]);
write_group(&mut track, 2, &[ts(200_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 3, "Expected group 0 (2 frames) + group 2 (1 frame)");
}
#[tokio::test]
async fn group_error_skips_to_next() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
group0.abort(moq_net::Error::Cancel).unwrap();
write_group(&mut track, 1, &[ts(30_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 1);
}
#[tokio::test]
async fn finished_group_aborted_mid_read_drains_then_continues() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
for timestamp in [ts(0), ts(10_000)] {
let frame = Frame {
timestamp,
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
};
Container::Legacy(crate::container::Kind::Data)
.write(&mut group0, &[frame])
.unwrap();
}
group0.finish().unwrap();
write_group(&mut track, 1, &[ts(30_000)]);
track.finish().unwrap();
let first = consumer.read().await.unwrap().unwrap();
assert_eq!(first.timestamp, ts(0));
group0.abort(moq_net::Error::Old).unwrap();
let rest = read_all(&mut consumer).await.unwrap();
assert_eq!(rest.len(), 2, "expected the held frame then group 1, got {rest:?}");
assert_eq!(rest[0].timestamp, ts(10_000));
assert_eq!(rest[1].timestamp, ts(30_000));
}
#[tokio::test]
async fn oversized_group_skips_to_next() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
let first = Frame {
timestamp: ts(0),
payload: Bytes::from(vec![0xDEu8; 1024]),
keyframe: false,
duration: None,
};
Container::Legacy(crate::container::Kind::Data)
.write(&mut group0, &[first])
.unwrap();
let overflow = Frame {
timestamp: ts(0),
payload: Bytes::from(vec![0u8; (moq_net::group::MAX_CACHE_BYTES - 1024) as usize]),
keyframe: false,
duration: None,
};
let overflowed = Container::Legacy(crate::container::Kind::Data).write(&mut group0, &[overflow]);
assert!(
matches!(
overflowed,
Err(crate::Error::Hang(hang::Error::Moq(moq_net::Error::GroupTooLarge)))
),
"oversized group must abort as GroupTooLarge, got {overflowed:?}"
);
write_group(&mut track, 1, &[ts(30_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 1, "expected group 1 only, got {} frames", frames.len());
assert_eq!(frames[0].timestamp, ts(30_000));
}
#[tokio::test]
async fn track_finishes_while_reading() {
tokio::time::pause();
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
write_group(&mut track, 0, &[ts(0)]);
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(20)).await;
write_group(&mut track, 1, &[ts(30_000)]);
tokio::time::sleep(Duration::from_millis(20)).await;
track.finish().unwrap();
});
let frames = tokio::time::timeout(Duration::from_secs(2), async {
let mut frames = Vec::new();
while let Some(frame) = consumer.read().await.unwrap() {
frames.push(frame);
}
frames
})
.await
.expect("consumer should not hang");
assert_eq!(frames.len(), 2);
finisher.await.unwrap();
}
#[tokio::test]
async fn empty_group_advances() {
let mut track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
group0.finish().unwrap();
write_group(&mut track, 1, &[ts(30_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
assert_eq!(frames.len(), 1);
}
#[tokio::test]
async fn video_container_legacy() {
tokio::time::pause();
let track = track_producer("video", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_millis(500)));
let mut consumer = Consumer::new(consumer_track, Container::Legacy(crate::container::Kind::Data));
let mut group = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
for i in 0..3u64 {
let frame = Frame {
timestamp: ts(i * 33_333),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
};
Container::Legacy(crate::container::Kind::Data)
.write(&mut group, &[frame])
.unwrap();
}
group.finish().unwrap();
track.finish().unwrap();
let mut frames = Vec::new();
while let Some(frame) = consumer.read().await.unwrap() {
frames.push(frame);
}
assert_eq!(frames.len(), 3);
assert_eq!(frames[0].timestamp, ts(0));
assert!(frames[0].keyframe);
assert_eq!(frames[1].timestamp, ts(33_333));
assert!(!frames[1].keyframe);
assert_eq!(frames[2].timestamp, ts(66_666));
assert!(!frames[2].keyframe);
}
#[tokio::test]
async fn duration_skip_advances_to_next_group() {
tokio::time::pause();
let track = track_producer("test", None);
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(10)));
let mut consumer = Consumer::new(consumer_track, DurationWire);
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
write_duration_frame(&mut group0, ts(0), ts(33_000));
let mut group1 = track.create_group(moq_net::group::Info { sequence: 1 }).unwrap();
write_duration_frame(&mut group1, ts(33_000), ts(33_000));
group1.finish().unwrap();
track.finish().unwrap();
let frames = tokio::time::timeout(Duration::from_secs(2), async {
let mut frames = Vec::new();
while let Some(frame) = consumer.read().await.unwrap() {
frames.push(frame);
}
frames
})
.await
.expect("consumer hung — duration skip regression");
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].timestamp, ts(0));
assert_eq!(frames[1].timestamp, ts(33_000));
assert_eq!(consumer.discontinuity(), 0, "a contiguous duration skip is not a hole");
drop(group0);
}
#[tokio::test]
async fn a_nonsequential_contiguous_jump_does_not_bump_playhead() {
let track = track_producer("test", None);
let mut consumer = Consumer::new(
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(10))),
DurationWire,
);
let mut group0 = track
.create_group(moq_net::group::Info { sequence: 1_000_000 })
.unwrap();
write_duration_frame(&mut group0, ts(0), ts(33_000));
group0.finish().unwrap();
let mut group1 = track
.create_group(moq_net::group::Info { sequence: 1_090_000 })
.unwrap();
write_duration_frame(&mut group1, ts(33_000), ts(33_000));
group1.finish().unwrap();
track.finish().unwrap();
let mut frames = Vec::new();
while let Some(frame) = consumer.read().await.unwrap() {
frames.push(frame);
}
assert_eq!(frames.len(), 2);
assert_eq!(consumer.discontinuity(), 0);
}
#[tokio::test]
async fn duration_below_gap_does_not_skip() {
tokio::time::pause();
let track = track_producer("test", None);
let consumer_track =
track.subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(10)));
let mut consumer = Consumer::new(consumer_track, DurationWire);
let mut group0 = track.create_group(moq_net::group::Info { sequence: 0 }).unwrap();
write_duration_frame(&mut group0, ts(0), ts(10_000));
let mut group1 = track.create_group(moq_net::group::Info { sequence: 1 }).unwrap();
write_duration_frame(&mut group1, ts(33_000), ts(33_000));
group1.finish().unwrap();
track.finish().unwrap();
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(20)).await;
write_duration_frame(&mut group0, ts(20_000), ts(10_000));
group0.finish().unwrap();
});
let frames = tokio::time::timeout(Duration::from_secs(2), async {
let mut frames = Vec::new();
while let Some(frame) = consumer.read().await.unwrap() {
frames.push(frame);
}
frames
})
.await
.expect("consumer hung");
assert_eq!(frames.len(), 3);
assert_eq!(frames[0].timestamp, ts(0));
assert_eq!(frames[1].timestamp, ts(20_000));
assert_eq!(frames[2].timestamp, ts(33_000));
finisher.await.unwrap();
}
#[tokio::test]
async fn live_duration_marker_follows_an_immediately_delivered_frame() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut consumer = Consumer::new(track.subscribe(None), Container::Legacy(crate::container::Kind::Video));
let mut group = track.append_group().unwrap();
Container::Legacy(crate::container::Kind::Video)
.write(
&mut group,
&[Frame {
timestamp: ts(0),
payload: Bytes::from_static(b"video"),
keyframe: true,
duration: None,
}],
)
.unwrap();
let frame = tokio::time::timeout(Duration::from_secs(1), consumer.read())
.await
.unwrap()
.unwrap()
.unwrap();
assert_eq!(frame.timestamp, ts(0));
write_marker(&mut group, ts(15_000));
let event = kio::wait(|waiter| consumer.poll_event(waiter)).await.unwrap();
assert!(matches!(event, Some(Event::FrameEnd(end)) if end == ts(15_000)));
}
}