use std::collections::VecDeque;
use std::task::{Poll, ready};
use super::Timestamp;
use super::{Container, Frame, Read};
pub struct Consumer<F: Container> {
track: moq_net::TrackConsumer,
format: F,
current: u64,
pending: VecDeque<GroupBuffer>,
startup: bool,
latency: std::time::Duration,
rewind: Rewind,
}
#[derive(Default)]
struct Rewind {
live_edge: Option<(u64, Timestamp)>,
boundary: Option<Reset>,
discontinuity: u64,
}
#[derive(Clone, Copy)]
struct Reset {
prev_max: u64,
group: u64,
timestamp: Timestamp,
}
impl Reset {
fn by_sequence(&self, sequence: u64) -> Option<bool> {
if sequence <= self.prev_max {
Some(true)
} else if sequence >= self.group {
Some(false)
} else {
None
}
}
fn is_stale(&self, sequence: u64, timestamp: Timestamp) -> bool {
self.by_sequence(sequence).unwrap_or(timestamp >= self.timestamp)
}
}
impl<F: Container> Consumer<F> {
pub fn new(track: moq_net::TrackConsumer, format: F) -> Self {
Self {
track,
format,
current: 0,
pending: VecDeque::new(),
startup: true,
latency: std::time::Duration::ZERO,
rewind: Rewind::default(),
}
}
pub fn with_latency(mut self, latency: std::time::Duration) -> Self {
self.latency = latency;
self
}
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>> {
let finished = self.poll_read_finish(waiter)?.is_ready();
if self.startup {
for (i, group) in self.pending.iter_mut().enumerate() {
if !matches!(group.poll_min_timestamp(waiter, &self.format), Poll::Ready(Ok(_))) {
continue;
}
self.current = group.sequence;
self.startup = false;
self.pending.drain(0..i);
break;
}
}
loop {
if self.poll_reset(waiter)? {
continue;
}
self.poll_classify(waiter)?;
while let Some(group) = self.pending.front_mut()
&& group.sequence <= self.current
{
match group.poll_read(waiter, &self.format) {
Poll::Ready(Ok(Some(frame))) => {
let seq = group.group.sequence;
let ts = frame.timestamp;
if self.rewind.live_edge.is_none_or(|(_, high)| ts > high) {
self.rewind.live_edge = Some((seq, ts));
}
return Poll::Ready(Ok(Some(frame)));
}
Poll::Pending => break,
Poll::Ready(Err(e)) => {
if !group.poll_aborted(waiter) {
return Poll::Ready(Err(e));
}
tracing::warn!(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);
}
Poll::Ready(Ok(None)) => {
self.pending.pop_front();
self.current += 1;
}
}
}
let (oldest_timestamp, 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(ts)) => (Some(std::time::Duration::from(ts)), current.max_end),
_ => (None, None),
}
} else {
(None, 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; }
}
let should_skip = if let Some((_, next_start)) = next_group {
if let Some(oldest) = oldest_timestamp {
let over_latency = max_timestamp.saturating_sub(oldest) >= self.latency;
let covered = current_end.is_some_and(|end| end >= next_start);
over_latency || covered
} else {
finished || self.pending.front().is_some_and(|g| g.sequence > self.current)
}
} else {
false
};
if let Some((new_idx, _)) = next_group
&& should_skip
{
self.pending.drain(0..new_idx);
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;
continue;
}
if finished && self.pending.is_empty() {
return Poll::Ready(Ok(None));
}
return Poll::Pending;
}
}
fn poll_read_finish(&mut self, waiter: &kio::Waiter) -> Poll<Result<(), F::Error>> {
loop {
let Some(group) = ready!(self.track.poll_recv_group(waiter)?) else {
return Poll::Ready(Ok(()));
};
let reader = GroupBuffer::new(group);
let sequence = reader.group.sequence;
let drop = match &self.rewind.boundary {
Some(reset) => match reset.by_sequence(sequence) {
Some(true) => true, Some(false) => sequence < self.current, None => false, },
None => sequence < self.current,
};
if drop {
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_reset(&mut self, waiter: &kio::Waiter) -> Result<bool, F::Error> {
let Some((prev_max, live_edge)) = self.rewind.live_edge else {
return Ok(false);
};
let reset = {
let mut found = None;
for group in self.pending.iter_mut().rev() {
if group.group.sequence <= self.current {
break;
}
let Poll::Ready(Ok(min)) = group.poll_min_timestamp(waiter, &self.format) else {
continue;
};
if min < live_edge {
found = Some(Reset {
prev_max,
group: group.group.sequence,
timestamp: min,
});
break;
}
}
let Some(reset) = found else {
return Ok(false);
};
reset
};
self.pending.retain(|g| match reset.by_sequence(g.group.sequence) {
Some(stale) => !stale,
None => g.min_timestamp.is_none_or(|ts| !reset.is_stale(g.group.sequence, ts)),
});
self.rewind.discontinuity += 1;
tracing::debug!(
prev_max = reset.prev_max,
group = reset.group,
discontinuity = self.rewind.discontinuity,
"buffer reset: group timestamps rewound"
);
self.rewind.boundary = Some(reset);
self.current = self.pending.front().map_or(reset.group, |g| g.group.sequence);
self.rewind.live_edge = Some((reset.group, reset.timestamp));
Ok(true)
}
fn poll_classify(&mut self, waiter: &kio::Waiter) -> Result<(), F::Error> {
let Some(reset) = self.rewind.boundary else {
return Ok(());
};
let mut i = 0;
while i < self.pending.len() {
let group = &mut self.pending[i];
if reset.by_sequence(group.group.sequence).is_some() {
i += 1;
continue;
}
match group.poll_min_timestamp(waiter, &self.format) {
Poll::Ready(Ok(min)) if reset.is_stale(group.group.sequence, min) => {
self.pending.remove(i);
}
_ => i += 1,
}
}
Ok(())
}
pub async fn closed(&self) -> Result<(), F::Error> {
Ok(self.track.closed().await?)
}
}
struct GroupBuffer {
group: moq_net::GroupConsumer,
index: usize,
buffered: VecDeque<Frame>,
min_timestamp: Option<Timestamp>,
max_timestamp: Option<Timestamp>,
max_end: Option<std::time::Duration>,
}
impl GroupBuffer {
fn new(group: moq_net::GroupConsumer) -> Self {
Self {
group,
index: 0,
buffered: VecDeque::new(),
max_timestamp: None,
min_timestamp: None,
max_end: None,
}
}
fn poll_read<F: Container>(&mut self, waiter: &kio::Waiter, format: &F) -> Poll<Result<Option<Frame>, F::Error>> {
if let Some(frame) = self.buffered.pop_front() {
return Poll::Ready(Ok(Some(frame)));
}
match ready!(self.buffer_one(waiter, format)?) {
true => Poll::Ready(Ok(Some(self.buffered.pop_front().unwrap()))),
false => Poll::Ready(Ok(None)),
}
}
fn buffer_once<F: Container>(&mut self, waiter: &kio::Waiter, format: &F) -> Poll<Result<bool, F::Error>> {
match ready!(format.poll_read(&mut self.group, waiter)?) {
Read::Done => return Poll::Ready(Ok(false)),
Read::Frame(frame) => self.ingest(frame),
Read::Fragment(frames) => {
for frame in frames {
self.ingest(frame);
}
}
}
Poll::Ready(Ok(true))
}
fn ingest(&mut self, mut frame: Frame) {
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,
});
let duration = frame.duration.map(std::time::Duration::from).unwrap_or_default();
let end = std::time::Duration::from(frame.timestamp) + duration;
self.max_end = Some(match self.max_end {
Some(existing) => existing.max(end),
None => end,
});
frame.keyframe = frame.keyframe || self.index == 0;
self.index += 1;
self.buffered.push_back(frame);
}
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(_)))
}
}
impl std::ops::Deref for GroupBuffer {
type Target = moq_net::GroupConsumer;
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<String>) -> moq_net::TrackProducer {
moq_net::Broadcast::new()
.produce()
.create_track(moq_net::Track::new(name))
.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::GroupProducer, frames: &[Frame]) -> Result<(), Self::Error> {
for frame in frames {
group.write_frame(encode_duration_frame(frame.timestamp, ts(0)))?;
}
Ok(())
}
fn poll_read(
&self,
group: &mut moq_net::GroupConsumer,
waiter: &kio::Waiter,
) -> Poll<Result<Read, Self::Error>> {
use bytes::Buf;
let Some(mut data) = ready!(group.poll_read_frame(waiter)?) else {
return Poll::Ready(Ok(Read::Done));
};
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(Read::Frame(Frame {
timestamp,
payload,
keyframe: false,
duration: Some(duration),
})))
}
}
fn write_duration_frame(group: &mut moq_net::GroupProducer, timestamp: Timestamp, duration: Timestamp) {
group.write_frame(encode_duration_frame(timestamp, duration)).unwrap();
}
fn write_group(track: &mut moq_net::TrackProducer, sequence: u64, timestamps: &[Timestamp]) {
let mut group = track.create_group(moq_net::Group { sequence }).unwrap();
for ×tamp in timestamps {
let frame = Frame {
timestamp,
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
duration: None,
};
Container::Legacy.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)
}
#[tokio::test]
async fn read_single_group() {
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
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());
}
#[tokio::test]
async fn read_multiple_frames_single_group() {
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
for f in 0..5u64 {
Container::Legacy
.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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::ZERO);
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
Container::Legacy
.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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
Container::Legacy
.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 reset_classifies_out_of_order_groups() {
let reset = Reset {
prev_max: 55,
group: 58,
timestamp: ts(90),
};
assert!(!reset.is_stale(57, ts(88)));
assert!(reset.is_stale(52, ts(86)));
assert!(reset.is_stale(56, ts(105)));
assert!(!reset.is_stale(58, ts(90)));
assert!(!reset.is_stale(59, ts(92)));
assert!(reset.is_stale(55, ts(100)));
}
#[tokio::test]
async fn reset_keeps_out_of_order_new_group() {
tokio::time::pause();
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
write_group(&mut track, 0, &[ts(0)]);
write_group(&mut track, 1, &[ts(100_000)]);
write_group(&mut track, 2, &[ts(200_000)]);
write_group(&mut track, 5, &[ts(3_000)]);
let finisher = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
write_group(&mut track, 3, &[ts(1_000)]);
write_group(&mut track, 4, &[ts(2_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!(micros.contains(&100_000), "old epoch played before the reset");
assert!(
micros.contains(&1_000) && micros.contains(&2_000) && micros.contains(&3_000),
"out-of-order new-epoch groups kept, got {micros:?}"
);
assert_eq!(consumer.rewind.discontinuity, 1, "one rewind detected");
finisher.await.expect("finisher task panicked");
}
#[tokio::test]
async fn reset_detected_behind_forward_newest_group() {
tokio::time::pause();
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
write_group(&mut track, 0, &[ts(0)]);
write_group(&mut track, 1, &[ts(100_000)]);
write_group(&mut track, 2, &[ts(200_000)]);
write_group(&mut track, 6, &[ts(250_000)]);
write_group(&mut track, 5, &[ts(50_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!(
consumer.rewind.discontinuity, 1,
"rewind detected behind a forward newest group"
);
assert!(micros.contains(&50_000), "resumed at the rewound group, got {micros:?}");
assert!(
!micros.contains(&200_000),
"the reneged tail was dropped, got {micros:?}"
);
}
#[tokio::test]
async fn backwards_timestamp_resets_buffer() {
tokio::time::pause();
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
for i in 0..5u64 {
write_group(&mut track, i, &[ts(i * 100_000)]);
}
write_group(&mut track, 5, &[ts(0), ts(20_000)]);
track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
let timestamps: Vec<_> = frames.iter().map(|f| f.timestamp).collect();
assert_eq!(timestamps, vec![ts(0), ts(100_000), ts(0), ts(20_000)]);
assert_eq!(consumer.rewind.discontinuity, 1);
}
#[tokio::test]
async fn backwards_timestamp_always_resets() {
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
write_group(&mut track, 0, &[ts(0)]);
write_group(&mut track, 1, &[ts(500_000)]);
write_group(&mut track, 2, &[ts(0)]); track.finish().unwrap();
let frames = read_all(&mut consumer).await.unwrap();
let timestamps: Vec<_> = frames.iter().map(|f| f.timestamp).collect();
assert_eq!(timestamps, vec![ts(0), ts(500_000), ts(0)]);
assert_eq!(
consumer.rewind.discontinuity, 1,
"the backwards group triggered a reset"
);
}
#[tokio::test]
async fn groups_delivered_in_sequence_order() {
tokio::time::pause();
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
Container::Legacy
.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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
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 mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
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 closed_resolves_when_track_ends() {
tokio::time::pause();
let mut track = track_producer("test");
let consumer_track = track.consume();
let consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
assert!(
tokio::time::timeout(Duration::from_millis(50), consumer.closed())
.await
.is_err()
);
track.finish().unwrap();
drop(track);
tokio::time::timeout(Duration::from_millis(200), consumer.closed())
.await
.expect("timeout expired waiting for closed()")
.expect("consumer.closed() returned an error");
}
#[tokio::test]
async fn gap_in_group_sequence_recovery() {
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(80));
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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
Container::Legacy
.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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
write_group(&mut track, 0, &[ts(0), ts(20_000)]);
write_group(&mut track, 2, &[ts(200_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");
}
struct FailingDecode;
impl ContainerTrait for FailingDecode {
type Error = crate::Error;
fn write(&self, group: &mut moq_net::GroupProducer, frames: &[Frame]) -> Result<(), Self::Error> {
for frame in frames {
group.write_frame(frame.payload.clone())?;
}
Ok(())
}
fn poll_read(
&self,
group: &mut moq_net::GroupConsumer,
waiter: &kio::Waiter,
) -> Poll<Result<Read, Self::Error>> {
use bytes::Buf;
let Some(mut data) = ready!(group.poll_read_frame(waiter)?) else {
return Poll::Ready(Ok(Read::Done));
};
if data.as_ref() == b"FAIL" {
return Poll::Ready(Err(crate::Error::UnknownFormat("malformed payload".into())));
}
Poll::Ready(Ok(Read::Frame(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 mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, FailingDecode);
let mut group = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
group.write_frame(Bytes::from(0u64.to_le_bytes().to_vec())).unwrap();
group.write_frame(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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
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 mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
let payload_bytes = vec![0x01, 0x02, 0x03, 0x04, 0x05];
let mut group = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
Container::Legacy
.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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
Container::Legacy
.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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(3700));
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_latency_changes_behavior() {
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10));
write_group(&mut track, 0, &[ts(0)]);
track.finish().unwrap();
let frame = consumer.read().await.unwrap().unwrap();
assert_eq!(frame.timestamp, ts(0));
consumer.latency = 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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(110));
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
for ×tamp in &[ts(0), ts(66_000), ts(33_000)] {
Container::Legacy
.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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
write_group(&mut track, 3, &[ts(0)]);
write_group(&mut track, 5, &[ts(150_000)]);
let mut group7 = track.create_group(moq_net::Group { sequence: 7 }).unwrap();
Container::Legacy
.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
.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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
let _group5 = track.create_group(moq_net::Group { sequence: 5 }).unwrap();
write_group(&mut track, 7, &[ts(210_000)]);
track.finish().unwrap();
let frames = tokio::time::timeout(Duration::from_millis(500), async {
let mut frames = Vec::new();
while let Some(frame) = consumer.read().await.unwrap() {
frames.push(frame);
}
frames
})
.await
.expect("should not hang");
assert!(!frames.is_empty());
}
#[tokio::test]
async fn startup_single_group_mid_stream() {
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
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 multiple_sequential_latency_skips() {
tokio::time::pause();
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(50));
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
Container::Legacy
.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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
Container::Legacy
.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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100));
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
Container::Legacy
.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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
let mut group0 = track.create_group(moq_net::Group { 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 track_finishes_while_reading() {
tokio::time::pause();
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
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");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
let mut group0 = track.create_group(moq_net::Group { 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 mut track = track_producer("video");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500));
let mut group = track.create_group(moq_net::Group { 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.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 mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, DurationWire).with_latency(Duration::from_secs(10));
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
write_duration_frame(&mut group0, ts(0), ts(33_000));
let mut group1 = track.create_group(moq_net::Group { 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));
drop(group0);
}
#[tokio::test]
async fn duration_below_gap_does_not_skip() {
tokio::time::pause();
let mut track = track_producer("test");
let consumer_track = track.consume();
let mut consumer = Consumer::new(consumer_track, DurationWire).with_latency(Duration::from_secs(10));
let mut group0 = track.create_group(moq_net::Group { sequence: 0 }).unwrap();
write_duration_frame(&mut group0, ts(0), ts(10_000));
let mut group1 = track.create_group(moq_net::Group { 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();
}
}