use bytes::Bytes;
use super::{Container, Frame, Kind};
fn add_micros(timestamp: moq_net::Timestamp, extra: moq_net::Timestamp) -> Option<moq_net::Timestamp> {
let micros = timestamp.as_micros().saturating_add(extra.as_micros());
let micros = u64::try_from(micros).ok()?;
moq_net::Timestamp::from_micros(micros).ok()
}
fn timestamp_lt(left: moq_net::Timestamp, right: moq_net::Timestamp) -> bool {
left.as_micros() < right.as_micros()
}
pub struct Producer<C: Container, R = ()> {
inner: moq_net::track::Producer,
container: C,
group: Option<moq_net::group::Producer>,
buffer: Vec<Frame>,
buffer_duration: std::time::Duration,
pending_sequence: Option<u64>,
recorder: Option<crate::timeline::Recorder>,
end: Option<moq_net::Timestamp>,
live_edge: Option<moq_net::Timestamp>,
last_duration: Option<moq_net::Timestamp>,
previous_timestamp: Option<moq_net::Timestamp>,
cadence: Option<moq_net::Timestamp>,
reordered: bool,
estimator: crate::catalog::Estimator,
bandwidth: Option<crate::catalog::Claim>,
rendition: Option<Box<dyn Rendition<R>>>,
}
trait Rendition<R>: Send {
fn name(&self) -> &str;
fn timestamp(&self, hint: Option<moq_net::Timestamp>) -> crate::Result<moq_net::Timestamp>;
fn set(&mut self, config: R) -> crate::Result<()>;
fn config(&self) -> crate::Result<R>;
fn replace(&mut self, config: R) -> crate::Result<()>;
fn estimate(&mut self, estimate: crate::catalog::Estimate) -> crate::Result<()>;
}
impl<E, R> Rendition<R> for crate::catalog::tracks::Rendition<E, R>
where
E: crate::catalog::hang::CatalogExt,
R: crate::catalog::RenditionConfig<E>,
{
fn name(&self) -> &str {
self.name()
}
fn timestamp(&self, hint: Option<moq_net::Timestamp>) -> crate::Result<moq_net::Timestamp> {
self.timestamp(hint)
}
fn set(&mut self, config: R) -> crate::Result<()> {
self.set(config)
}
fn config(&self) -> crate::Result<R> {
self.config()
}
fn replace(&mut self, config: R) -> crate::Result<()> {
self.replace(config)
}
fn estimate(&mut self, estimate: crate::catalog::Estimate) -> crate::Result<()> {
self.estimate(estimate)
}
}
impl<C: Container> Producer<C> {
pub fn new(track: moq_net::track::Producer, container: C) -> Self {
Self {
inner: track,
container,
group: None,
buffer: Vec::new(),
buffer_duration: std::time::Duration::ZERO,
pending_sequence: None,
recorder: None,
end: None,
live_edge: None,
last_duration: None,
previous_timestamp: None,
cadence: None,
reordered: false,
estimator: crate::catalog::Estimator::new(),
bandwidth: None,
rendition: None,
}
}
}
impl<C: Container, R: Clone + Send + 'static> Producer<C, R>
where
crate::Error: From<C::Error>,
{
pub(crate) fn with_rendition<E>(
track: moq_net::track::Producer,
container: C,
rendition: crate::catalog::tracks::Rendition<E, R>,
) -> Self
where
E: crate::catalog::hang::CatalogExt,
R: crate::catalog::RenditionConfig<E>,
{
Self {
inner: track,
container,
group: None,
buffer: Vec::new(),
buffer_duration: std::time::Duration::ZERO,
pending_sequence: None,
recorder: None,
end: None,
live_edge: None,
last_duration: None,
previous_timestamp: None,
cadence: None,
reordered: false,
estimator: rendition.estimator(),
bandwidth: None,
rendition: Some(Box::new(rendition)),
}
}
pub fn set(&mut self, config: R) -> crate::Result<()> {
self.rendition.as_mut().ok_or(crate::Error::NotPublished)?.set(config)
}
pub fn modify(&mut self) -> crate::Result<Guard<'_, R>> {
let rendition = self.rendition.as_mut().ok_or(crate::Error::NotPublished)?;
let config = rendition.config()?;
Ok(Guard {
rendition: rendition.as_mut(),
config: Some(config),
})
}
pub fn timestamp(&self, hint: Option<moq_net::Timestamp>) -> crate::Result<moq_net::Timestamp> {
self.rendition
.as_ref()
.ok_or(crate::Error::NotPublished)?
.timestamp(hint)
}
fn publish_estimate(&mut self) -> crate::Result<()> {
if let Some(rendition) = self.rendition.as_mut() {
rendition.estimate(self.estimator.estimate())?;
}
Ok(())
}
pub fn flush(&mut self, timestamp: moq_net::Timestamp, now: std::time::Instant) -> crate::Result<()> {
self.estimator.flush(timestamp, now);
self.publish_estimate()
}
pub fn name(&self) -> &str {
self.rendition
.as_ref()
.map_or(self.inner.name(), |rendition| rendition.name())
}
pub fn demand(&self) -> moq_net::track::Demand {
self.inner.demand()
}
}
pub struct Guard<'a, R> {
rendition: &'a mut dyn Rendition<R>,
config: Option<R>,
}
impl<R> Guard<'_, R> {
pub fn commit(mut self) -> crate::Result<()> {
let config = self.config.take().expect("config is present until commit");
self.rendition.replace(config)
}
}
impl<R> std::ops::Deref for Guard<'_, R> {
type Target = R;
fn deref(&self) -> &Self::Target {
self.config.as_ref().expect("config is present until commit")
}
}
impl<R> std::ops::DerefMut for Guard<'_, R> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.config.as_mut().expect("config is present until commit")
}
}
impl<R> Drop for Guard<'_, R> {
fn drop(&mut self) {
let Some(config) = self.config.take() else {
return;
};
if let Err(err) = self.rendition.replace(config) {
tracing::error!(%err, "failed to publish modified rendition");
}
}
}
impl<C: Container, R: Clone + Send + 'static> Producer<C, R>
where
crate::Error: From<C::Error>,
{
#[cfg(test)]
fn bandwidth_ceiling(&self) -> Option<moq_net::bandwidth::Rate> {
self.bandwidth.as_ref().and_then(|claim| claim.ceiling())
}
pub fn estimate(&self) -> crate::catalog::Estimate {
self.estimator.estimate()
}
pub fn reorder(&mut self, delay: moq_net::Timestamp) {
self.estimator.reorder(delay);
}
pub(crate) fn burst(&mut self, duration: std::time::Duration) -> crate::Result<()> {
self.estimator.burst(duration);
self.publish_estimate()
}
pub fn needs_keyframe(&self) -> bool {
self.group.is_none()
}
pub fn with_buffer(mut self, duration: std::time::Duration) -> Self {
self.buffer_duration = duration;
self
}
pub fn with_recorder(mut self, recorder: crate::timeline::Recorder) -> Self {
self.recorder = Some(recorder);
self
}
pub fn with_bandwidth(mut self, allocator: moq_net::bandwidth::Allocator) -> Self {
self.bandwidth = Some(crate::catalog::Claim::new(allocator));
self
}
fn claim(&mut self) {
let Some(bandwidth) = self.bandwidth.as_mut() else {
return;
};
bandwidth.update(&self.inner.demand(), self.estimator.estimate().bitrate);
}
pub fn track(&self) -> &moq_net::track::Producer {
&self.inner
}
pub fn live_edge(&self) -> Option<moq_net::Timestamp> {
self.live_edge
}
pub(crate) fn floor(&self, keyframe: bool) -> Option<moq_net::Timestamp> {
match (self.live_edge, self.end.filter(|_| keyframe)) {
(Some(edge), Some(end)) if timestamp_lt(edge, end) => Some(end),
(edge, end) => edge.or(end),
}
}
pub fn write(&mut self, frame: Frame) -> crate::Result<()> {
if frame.keyframe {
let rewound = self
.previous_timestamp
.is_some_and(|previous| timestamp_lt(frame.timestamp, previous));
self.cut((!rewound).then_some(frame.timestamp))?;
if rewound {
self.cadence = None;
}
}
if self.live_edge.is_some_and(|edge| timestamp_lt(frame.timestamp, edge)) {
return Err(super::TimestampRewind.into());
}
if self.group.is_none() {
if !frame.keyframe {
return Err(super::MissingKeyframe.into());
}
let group = match self.pending_sequence.take() {
Some(sequence) => self.inner.create_group(moq_net::group::Info { sequence })?,
None => self.inner.append_group()?,
};
if let Some(recorder) = self.recorder.as_mut() {
recorder.record(group.sequence, frame.timestamp, frame.keyframe);
}
self.group = Some(group);
}
if self.buffer_duration.is_zero() {
let group = self.group.as_mut().unwrap();
let (timestamp, duration, bytes) = (frame.timestamp, frame.duration, frame.payload.len());
self.container.write(group, &[frame])?;
self.estimator.write(timestamp, bytes);
self.observe_end(timestamp, duration);
} else {
self.estimator.write(frame.timestamp, frame.payload.len());
self.observe_end(frame.timestamp, frame.duration);
self.buffer.push(frame);
if self.buffer.len() >= 2 {
let mut iter = self.buffer.iter().map(|f| std::time::Duration::from(f.timestamp));
let first = iter.next().unwrap();
let (min, max) = iter.fold((first, first), |(min, max), d| (min.min(d), max.max(d)));
if max.saturating_sub(min) >= self.buffer_duration {
self.flush_buffer(None)?;
}
}
}
Ok(())
}
pub fn cut(&mut self, end: Option<moq_net::Timestamp>) -> crate::Result<()> {
let marker_at = end.or_else(|| self.estimated_end());
self.close(end, marker_at)
}
fn close(&mut self, end: Option<moq_net::Timestamp>, marker_at: Option<moq_net::Timestamp>) -> crate::Result<()> {
if self.container.kind() == Kind::Video
&& !self.reordered
&& let Some((end, previous)) = end.zip(self.previous_timestamp)
&& timestamp_lt(end, previous)
{
return Err(super::InvalidEnd.into());
}
self.estimator.cut(end);
self.claim();
if let Some(recorder) = self.recorder.as_mut()
&& let Some(end) = marker_at.or(end).max(self.end)
{
recorder.end(end);
}
let tail_end = marker_at.filter(|_| !self.reordered);
self.flush_buffer(tail_end)?;
if let Some(group) = self.group.as_mut() {
self.container.finish_group(group, tail_end)?;
}
if let Some(group) = self.group.take() {
group.finish()?;
}
if let Some(end) = self.end {
self.raise_live_edge(end);
}
self.end = None;
self.last_duration = None;
self.previous_timestamp = None;
self.reordered = false;
self.publish_estimate()?;
Ok(())
}
fn raise_live_edge(&mut self, timestamp: moq_net::Timestamp) {
if self.live_edge.is_none_or(|edge| !timestamp_lt(timestamp, edge)) {
self.live_edge = Some(timestamp);
}
}
fn observe_end(&mut self, timestamp: moq_net::Timestamp, duration: Option<moq_net::Timestamp>) {
self.reordered |= self.previous_timestamp.is_some_and(|previous| timestamp < previous);
if let Some(previous) = self.previous_timestamp
&& let Ok(delta) = timestamp.checked_sub(previous)
&& !delta.is_zero()
{
self.cadence = Some(delta);
}
self.previous_timestamp = Some(timestamp);
let micros = timestamp.as_micros() + duration.map(|d| d.as_micros()).unwrap_or(0);
let Ok(micros) = u64::try_from(micros) else { return };
let Ok(end) = moq_net::Timestamp::from_micros(micros) else {
return;
};
if self.end.is_none_or(|prev| end >= prev) {
self.end = Some(end);
self.last_duration = duration.filter(|duration| !duration.is_zero());
}
}
fn estimated_end(&self) -> Option<moq_net::Timestamp> {
let last = self.end?;
if self.last_duration.is_some() {
return Some(last);
}
add_micros(last, self.cadence?)
}
pub fn seek(&mut self, sequence: u64) -> crate::Result<()> {
self.cut(None)?;
self.pending_sequence = Some(sequence);
Ok(())
}
pub fn discontinuity(&mut self) -> crate::Result<()> {
self.close(None, None)?;
self.estimator.discontinuity();
self.cadence = None;
if self.container.kind() == Kind::Data {
let skipped = match self.pending_sequence.take() {
Some(sequence) => sequence,
None => self.inner.latest().map_or(0, |s| s + 1),
};
self.pending_sequence = Some(skipped + 1);
return Ok(());
}
let mut group = match self.pending_sequence.take() {
Some(sequence) => self.inner.create_group(moq_net::group::Info { sequence })?,
None => self.inner.append_group()?,
};
let timestamp = self.live_edge.unwrap_or(moq_net::Timestamp::ZERO);
if let Some(recorder) = self.recorder.as_mut() {
recorder.record(group.sequence, timestamp, false);
recorder.end(timestamp);
}
self.container.write(
&mut group,
&[Frame {
timestamp,
payload: Bytes::new(),
keyframe: false,
duration: None,
}],
)?;
group.finish()?;
Ok(())
}
fn flush_buffer(&mut self, next: Option<moq_net::Timestamp>) -> crate::Result<()> {
if self.buffer.is_empty() {
return Ok(());
}
for i in 0..self.buffer.len() {
if self.buffer[i].duration.is_some() {
continue;
}
let boundary = self.buffer.get(i + 1).map(|f| f.timestamp).or(next);
if let Some(boundary) = boundary
&& let Ok(boundary) = boundary.convert(self.buffer[i].timestamp.scale())
&& let Ok(duration) = boundary.checked_sub(self.buffer[i].timestamp)
{
self.buffer[i].duration = Some(duration);
}
}
let group = match &mut self.group {
Some(group) => group,
None => return Ok(()),
};
self.container.write(group, &self.buffer)?;
self.buffer.clear();
Ok(())
}
pub fn finish(&mut self) -> crate::Result<()> {
self.cut(None)?;
self.inner.finish()?;
self.publish_estimate()?;
Ok(())
}
pub fn abort(mut self, err: moq_net::Error) {
self.buffer.clear();
if let Some(group) = self.group.take() {
let _ = group.abort(err.clone());
}
let _ = self.inner.abort(err);
}
pub fn consume(&self) -> moq_net::track::Subscriber {
self.inner.subscribe(None)
}
}
impl<C: Container, R> std::ops::Deref for Producer<C, R> {
type Target = moq_net::track::Producer;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog::hang::Container;
use moq_net::Timestamp;
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 replay() -> moq_net::track::Subscription {
moq_net::track::Subscription::default().with_max_age(RECORDING_MAX_AGE)
}
const RECORDING_MAX_AGE: std::time::Duration = std::time::Duration::from_secs(30);
fn frame(timestamp_us: u64, keyframe: bool) -> Frame {
Frame {
timestamp: Timestamp::from_micros(timestamp_us).unwrap(),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe,
duration: None,
}
}
fn sized_frame(timestamp_us: u64, keyframe: bool, bytes: usize) -> Frame {
Frame {
payload: Bytes::from(vec![0u8; bytes]),
..frame(timestamp_us, keyframe)
}
}
#[tokio::test]
async fn writes_measure_bitrate_without_inventing_jitter() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Data));
for i in 0..80u64 {
producer.write(sized_frame(i * 25_000, i % 10 == 0, 5_000)).unwrap();
}
producer.finish().unwrap();
let estimate = producer.estimate();
assert_eq!(estimate.jitter, None, "PTS spacing alone is not flush delay");
assert_eq!(estimate.bitrate, Some(1_600_000));
}
#[test]
fn catalog_flush_measures_delay_across_renditions_and_jitter_within_each() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut tracks = Vec::new();
for name in ["fast", "slow"] {
let net = broadcast
.create_track(name, catalog.track_info(hang::catalog::PRIORITY.video))
.unwrap();
let config = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8);
let mut track = catalog
.video(net, Container::Legacy(crate::container::Kind::Video), config)
.unwrap();
track.write(frame(0, true)).unwrap();
tracks.push(track);
}
let anchor = std::time::Instant::now();
let ms = std::time::Duration::from_millis;
let pts = |millis: u64| Timestamp::from_micros(millis * 1_000).unwrap();
tracks[0].flush(pts(0), anchor).unwrap();
tracks[1].flush(pts(0), anchor + ms(200)).unwrap();
assert_eq!(catalog.snapshot().video.renditions["slow"].jitter, None);
assert_eq!(catalog.snapshot().video.renditions["slow"].delay, Some(ms(200)));
assert_eq!(catalog.snapshot().video.renditions["fast"].delay, None);
tracks[1].flush(pts(40), anchor + ms(300)).unwrap();
assert_eq!(catalog.snapshot().video.renditions["fast"].jitter, None);
assert_eq!(catalog.snapshot().video.renditions["slow"].jitter, Some(ms(60)));
}
#[tokio::test]
async fn a_passthrough_producer_ratchets_its_bandwidth_claim() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let _sub = track.consume();
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Data))
.with_bandwidth(moq_net::bandwidth::Allocator::unlimited());
producer.write(sized_frame(0, true, 100_000)).unwrap();
producer.write(sized_frame(500_000, false, 100_000)).unwrap();
assert_eq!(producer.bandwidth_ceiling(), None, "the first window has not closed");
producer.cut(Some(Timestamp::from_micros(1_000_000).unwrap())).unwrap();
assert_eq!(
producer.bandwidth_ceiling(),
Some(moq_net::bandwidth::Rate::from_bps(1_600_000))
);
producer.write(sized_frame(1_000_000, true, 25_000)).unwrap();
producer.cut(Some(Timestamp::from_micros(2_000_000).unwrap())).unwrap();
assert_eq!(
producer.bandwidth_ceiling(),
Some(moq_net::bandwidth::Rate::from_bps(1_600_000)),
"a quieter window never hands the room away"
);
producer.write(sized_frame(2_000_000, true, 250_000)).unwrap();
producer.cut(Some(Timestamp::from_micros(3_000_000).unwrap())).unwrap();
assert_eq!(
producer.bandwidth_ceiling(),
Some(moq_net::bandwidth::Rate::from_bps(2_000_000))
);
}
#[tokio::test]
async fn a_passthrough_peak_lowers_a_coresident_encoder_grant() {
let estimate = moq_net::bandwidth::Producer::new();
let allocator = moq_net::bandwidth::Allocator::new(estimate.consume());
estimate
.set(Some(moq_net::bandwidth::Rate::from_bps(6_000_000)))
.unwrap();
fn video_track() -> (moq_net::broadcast::Producer, moq_net::track::Producer) {
let broadcast = moq_net::broadcast::Info::new().produce();
let track = broadcast
.create_track("t", hang::container::track_info(hang::catalog::PRIORITY.video))
.unwrap();
(broadcast, track)
}
let (_encoder_broadcast, encoder_track) = video_track();
let _encoder_sub = encoder_track.consume();
let encoder = allocator.reserve(&encoder_track.demand(), moq_net::bandwidth::Rate::from_bps(8_000_000));
assert_eq!(
encoder.peek(),
Some(moq_net::bandwidth::Rate::from_bps(6_000_000)),
"alone, the encoder takes the whole estimate"
);
let (_passthrough_broadcast, passthrough) = video_track();
let _passthrough_sub = passthrough.consume();
let mut producer =
Producer::new(passthrough, Container::Legacy(crate::container::Kind::Data)).with_bandwidth(allocator);
producer.write(sized_frame(0, true, 100_000)).unwrap();
producer.write(sized_frame(500_000, false, 100_000)).unwrap();
assert_eq!(
encoder.peek(),
Some(moq_net::bandwidth::Rate::from_bps(6_000_000)),
"nothing claimed before the first window"
);
producer.cut(Some(Timestamp::from_micros(1_000_000).unwrap())).unwrap();
assert_eq!(
encoder.peek(),
Some(moq_net::bandwidth::Rate::from_bps(4_400_000)),
"the encoder's grant drops by the passthrough peak (1.6 Mbps)"
);
}
#[tokio::test]
async fn per_frame_groups_still_measure_bitrate() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Data));
for i in 0..40u64 {
let keyframe = producer.needs_keyframe();
producer.write(sized_frame(i * 40_000, keyframe, 5_000)).unwrap();
producer.cut(None).unwrap();
}
assert_eq!(producer.estimate().bitrate, Some(1_000_000));
}
#[tokio::test]
async fn discontinuity_is_not_measured_across() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Data));
producer.write(sized_frame(0, true, 5_000)).unwrap();
producer.discontinuity().unwrap();
producer.write(sized_frame(2_405_070_000, true, 5_000)).unwrap();
producer.finish().unwrap();
assert_eq!(producer.estimate().jitter, None);
assert_eq!(producer.estimate().bitrate, None);
}
#[derive(Clone)]
struct RejectAt(u64);
impl super::Container for RejectAt {
type Error = crate::Error;
fn write(&self, _group: &mut moq_net::group::Producer, frames: &[Frame]) -> Result<(), Self::Error> {
match frames.iter().any(|f| f.timestamp.as_micros() == self.0 as u128) {
true => Err(moq_net::Error::FrameTooLarge.into()),
false => Ok(()),
}
}
fn poll_read(
&self,
_group: &mut moq_net::group::Consumer,
_waiter: &kio::Waiter,
) -> std::task::Poll<Result<Option<Vec<Frame>>, Self::Error>> {
unreachable!("RejectAt is write-only")
}
}
#[tokio::test]
async fn a_rejected_frame_is_not_measured() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut producer = Producer::new(track, RejectAt(40_000));
for i in 0..40u64 {
let bytes = if i == 1 { 500_000 } else { 5_000 };
let result = producer.write(sized_frame(i * 40_000, true, bytes));
assert_eq!(result.is_err(), i == 1, "only the rejected frame fails");
}
assert_eq!(producer.estimate().bitrate, Some(1_000_000));
}
#[tokio::test]
async fn reorder_raises_the_measured_jitter() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Data));
producer.write(frame(0, true)).unwrap();
producer.write(frame(16_000, false)).unwrap();
assert_eq!(producer.estimate().jitter, None, "PTS spacing alone is not flush delay");
producer.reorder(Timestamp::from_micros(48_000).unwrap());
assert_eq!(
producer.estimate().jitter,
Some(std::time::Duration::from_millis(48)),
"a B-frame stream needs the deeper decode buffer"
);
}
async fn collect_groups(mut consumer: moq_net::track::Subscriber) -> Vec<usize> {
let mut groups = Vec::new();
while let Some(mut group) = consumer.recv_group().await.unwrap() {
let mut count = 0;
while group.next_frame().await.unwrap().is_some() {
count += 1;
}
groups.push(count);
}
groups
}
#[tokio::test]
async fn discontinuity_publishes_an_empty_group() {
let discontinuity_max_age = std::time::Duration::from_secs(41 * 60);
let info = hang::container::track_info(hang::catalog::PRIORITY.audio).with_max_age(discontinuity_max_age);
let track = track_producer("test", info);
let consumer = track.subscribe(moq_net::track::Subscription::default().with_max_age(discontinuity_max_age));
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Audio));
producer.write(frame(0, true)).unwrap();
producer.write(frame(10_000, false)).unwrap();
producer.discontinuity().unwrap();
producer.write(frame(2_405_070_000, true)).unwrap();
producer.finish().unwrap();
let groups = collect_payloads(consumer).await;
assert_eq!(groups.len(), 3);
assert_eq!(groups[0], vec![(0, 2), (10_000, 2)]);
assert_eq!(
groups[1],
vec![(10_000, 0)],
"the marker is one empty frame at the exclusive end"
);
assert_eq!(groups[2][0], (2_405_070_000, 2));
}
#[tokio::test]
async fn discontinuity_moves_the_live_edge_off_stale_content() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.audio));
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Audio));
producer.write(frame(0, true)).unwrap();
let stale = producer.track().latest();
producer.discontinuity().unwrap();
let edge = producer.track().latest();
assert_ne!(edge, stale, "the marker group is the live edge now");
assert_eq!(edge, stale.map(|s| s + 1));
}
#[tokio::test]
async fn keyframe_closes_group_immediately() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Data));
producer.write(frame(0, true)).unwrap(); producer.write(frame(10_000, false)).unwrap();
producer.write(frame(20_000, true)).unwrap(); producer.write(frame(30_000, false)).unwrap();
producer.finish().unwrap();
assert_eq!(collect_groups(consumer).await, vec![2, 2]);
}
#[tokio::test]
async fn needs_keyframe_drives_audio_grouping() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Data));
for ts in [0, 10_000, 20_000] {
let keyframe = producer.needs_keyframe();
producer.write(frame(ts, keyframe)).unwrap();
}
producer.cut(None).unwrap();
for ts in [30_000, 40_000] {
let keyframe = producer.needs_keyframe();
producer.write(frame(ts, keyframe)).unwrap();
}
producer.finish().unwrap();
assert_eq!(collect_groups(consumer).await, vec![3, 2]);
}
async fn collect_payloads(mut consumer: moq_net::track::Subscriber) -> Vec<Vec<(u128, usize)>> {
let mut groups = Vec::new();
while let Some(mut group) = consumer.recv_group().await.unwrap() {
let mut frames = Vec::new();
while let Some(frame) = group.read_frame().await.unwrap() {
let decoded = hang::container::Frame::decode(frame.payload).unwrap();
frames.push((decoded.timestamp.as_micros(), decoded.payload.len()));
}
groups.push(frames);
}
groups
}
#[tokio::test]
async fn cut_writes_a_duration_marker_at_the_callers_bound() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video));
producer.write(frame(0, true)).unwrap();
producer.write(frame(10_000, false)).unwrap();
producer
.cut(Some(moq_net::Timestamp::from_micros(15_000).unwrap()))
.unwrap();
producer.write(frame(20_000, true)).unwrap();
producer.finish().unwrap();
let groups = collect_payloads(consumer).await;
assert_eq!(groups[0], vec![(0, 2), (10_000, 2), (15_000, 0)]);
assert_eq!(groups[1][0], (20_000, 2));
assert_eq!(groups[1].last().unwrap().1, 0, "finish closes the last group");
}
#[tokio::test]
async fn audio_cut_writes_no_duration_marker() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.audio));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Audio));
producer.write(frame(0, true)).unwrap();
producer.write(frame(20_000, false)).unwrap();
producer.finish().unwrap();
assert_eq!(collect_payloads(consumer).await, vec![vec![(0, 2), (20_000, 2)]]);
}
async fn collect_loc_payloads(mut consumer: moq_net::track::Subscriber) -> Vec<Vec<(u64, usize)>> {
let mut groups = Vec::new();
while let Some(mut group) = consumer.recv_group().await.unwrap() {
let mut frames = Vec::new();
while let Some(frame) = group.read_frame().await.unwrap() {
let decoded = moq_loc::decode(frame.payload).unwrap();
frames.push((decoded.timestamp, decoded.payload.len()));
}
groups.push(frames);
}
groups
}
#[tokio::test]
async fn loc_cut_writes_a_duration_marker_at_the_callers_bound() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Loc(crate::container::Kind::Video));
producer.write(frame(0, true)).unwrap();
producer.write(frame(10_000, false)).unwrap();
producer
.cut(Some(moq_net::Timestamp::from_micros(15_000).unwrap()))
.unwrap();
producer.write(frame(20_000, true)).unwrap();
producer.finish().unwrap();
let groups = collect_loc_payloads(consumer).await;
assert_eq!(groups[0], vec![(0, 2), (10_000, 2), (15_000, 0)]);
assert_eq!(groups[1][0], (20_000, 2));
assert_eq!(groups[1].last().unwrap().1, 0, "finish closes the last group");
}
#[tokio::test]
async fn loc_audio_writes_no_duration_marker() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.audio));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Loc(crate::container::Kind::Audio));
producer.write(frame(0, true)).unwrap();
producer.write(frame(20_000, false)).unwrap();
producer
.cut(Some(moq_net::Timestamp::from_micros(30_000).unwrap()))
.unwrap();
producer.write(frame(40_000, true)).unwrap();
producer.finish().unwrap();
assert_eq!(
collect_loc_payloads(consumer).await,
vec![vec![(0, 2), (20_000, 2)], vec![(40_000, 2)]]
);
}
#[tokio::test]
async fn loc_data_writes_no_duration_marker() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Loc(crate::container::Kind::Data));
producer.write(frame(0, true)).unwrap();
producer
.cut(Some(moq_net::Timestamp::from_micros(15_000).unwrap()))
.unwrap();
producer.write(frame(20_000, true)).unwrap();
producer.finish().unwrap();
assert_eq!(
collect_loc_payloads(consumer).await,
vec![vec![(0, 2)], vec![(20_000, 2)]]
);
}
#[tokio::test]
async fn cut_closes_immediately() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Data));
producer.write(frame(0, true)).unwrap();
producer.write(frame(10_000, false)).unwrap();
producer
.cut(Some(moq_net::Timestamp::from_micros(15_000).unwrap()))
.unwrap();
producer.write(frame(20_000, true)).unwrap();
producer.finish().unwrap();
assert_eq!(collect_groups(consumer).await, vec![2, 1]);
}
#[test]
fn first_frame_must_be_keyframe() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Data));
let err = producer.write(frame(0, false)).unwrap_err();
assert!(matches!(err, crate::Error::MissingKeyframe(_)));
}
async fn collect_sequences(mut consumer: moq_net::track::Subscriber) -> Vec<u64> {
let mut sequences = Vec::new();
while let Some(group) = consumer.recv_group().await.unwrap() {
sequences.push(group.sequence);
}
sequences
}
#[tokio::test]
async fn seek_uses_explicit_sequence() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Data));
producer.write(frame(0, true)).unwrap(); producer.seek(42).unwrap();
producer.write(frame(10_000, true)).unwrap(); producer.finish().unwrap();
assert_eq!(collect_sequences(consumer).await, vec![0, 42]);
}
#[tokio::test]
async fn seek_clears_pending_after_use() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Data));
producer.seek(5).unwrap();
producer.write(frame(0, true)).unwrap(); producer.write(frame(10_000, true)).unwrap(); producer.finish().unwrap();
assert_eq!(collect_sequences(consumer).await, vec![5, 6]);
}
#[derive(Clone, Default)]
struct Recording(std::rc::Rc<std::cell::RefCell<Vec<Vec<Frame>>>>);
impl super::Container for Recording {
type Error = crate::Error;
fn write(&self, _group: &mut moq_net::group::Producer, frames: &[Frame]) -> Result<(), Self::Error> {
self.0.borrow_mut().push(frames.to_vec());
Ok(())
}
fn poll_read(
&self,
_group: &mut moq_net::group::Consumer,
_waiter: &kio::Waiter,
) -> std::task::Poll<Result<Option<Vec<Frame>>, Self::Error>> {
unreachable!("Recording is write-only")
}
}
#[tokio::test]
async fn keyframe_backfills_batched_durations() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let recording = Recording::default();
let mut producer = Producer::new(track, recording.clone()).with_buffer(std::time::Duration::from_secs(10));
producer.write(frame(0, true)).unwrap(); producer.write(frame(33_000, false)).unwrap(); producer.write(frame(66_000, true)).unwrap(); producer.finish().unwrap();
let writes = recording.0.borrow();
let group0 = &writes[0];
assert_eq!(group0.len(), 2);
assert_eq!(group0[0].duration, Some(Timestamp::from_micros(33_000).unwrap()));
assert_eq!(group0[1].duration, Some(Timestamp::from_micros(33_000).unwrap()));
}
#[tokio::test]
async fn cut_none_backfills_the_last_frame_duration_at_native_scale() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let recording = Recording::default();
let mut producer = Producer::new(track, recording.clone()).with_buffer(std::time::Duration::from_secs(10));
let scale = moq_net::Timescale::new(90_000).unwrap();
let frame_at = |ticks: u64, keyframe: bool| Frame {
timestamp: Timestamp::new(ticks, scale).unwrap(),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe,
duration: None,
};
producer.write(frame_at(0, true)).unwrap();
producer.write(frame_at(3003, false)).unwrap();
producer.write(frame_at(6006, false)).unwrap();
producer.cut(None).unwrap();
producer.finish().unwrap();
let writes = recording.0.borrow();
let group = &writes[0];
assert_eq!(group.len(), 3);
let duration = |ticks: u64| Timestamp::new(ticks, scale).unwrap();
assert_eq!(group[0].duration, Some(duration(3003)));
assert_eq!(group[1].duration, Some(duration(3003)));
assert_eq!(
group[2].duration,
Some(duration(3002)),
"the truncated estimate still lands on exact native ticks instead of going missing"
);
let info = crate::container::fmp4::FragmentInfo {
track_id: 1,
timescale: scale,
sequence_number: 0,
kind: crate::container::fmp4::Kind::Video,
};
crate::container::fmp4::encode_fragment(info, group).unwrap();
}
#[tokio::test]
async fn duration_marker_uses_cadence_not_batching_delay() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video));
producer.burst(std::time::Duration::from_secs(1)).unwrap();
producer.write(frame(0, true)).unwrap();
producer.write(frame(20_000, false)).unwrap();
producer.write(frame(40_000, false)).unwrap();
producer.finish().unwrap();
assert_eq!(collect_payloads(consumer).await[0].last(), Some(&(60_000, 0)));
}
#[tokio::test]
async fn reordered_group_does_not_mark_the_decode_tail_with_the_presentation_end() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video));
for (index, timestamp) in [0, 120_000, 40_000, 80_000].into_iter().enumerate() {
producer.write(frame(timestamp, index == 0)).unwrap();
}
producer.write(frame(160_000, true)).unwrap();
producer.write(frame(200_000, false)).unwrap();
producer.cut(Some(Timestamp::from_micros(240_000).unwrap())).unwrap();
producer.finish().unwrap();
let groups = collect_payloads(consumer).await;
assert_eq!(groups[0], vec![(0, 2), (120_000, 2), (40_000, 2), (80_000, 2)]);
assert_eq!(
groups[1].last(),
Some(&(240_000, 0)),
"the next group can mark its tail"
);
}
#[tokio::test]
async fn duration_marker_follows_a_slower_cadence() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video));
for (index, timestamp) in [0, 16_000, 32_000, 65_000, 98_000].into_iter().enumerate() {
producer.write(frame(timestamp, index == 0)).unwrap();
}
producer.finish().unwrap();
assert_eq!(collect_payloads(consumer).await[0].last(), Some(&(131_000, 0)));
}
#[tokio::test]
async fn an_unknown_tail_at_the_previous_endpoint_uses_current_cadence() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video));
let mut first = frame(0, true);
first.duration = Some(Timestamp::from_micros(40_000).unwrap());
producer.write(first).unwrap();
producer.write(frame(40_000, false)).unwrap();
producer.finish().unwrap();
assert_eq!(collect_payloads(consumer).await[0].last(), Some(&(80_000, 0)));
}
#[tokio::test(start_paused = true)]
async fn backwards_cut_is_rejected_before_flushing_or_closing() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video))
.with_buffer(std::time::Duration::from_secs(1));
producer.write(frame(20_000, true)).unwrap();
let mut group = consumer.recv_group().await.unwrap().unwrap();
assert!(producer.cut(Some(Timestamp::from_micros(10_000).unwrap())).is_err());
assert!(!producer.needs_keyframe());
assert!(
tokio::time::timeout(std::time::Duration::from_millis(1), group.read_frame())
.await
.is_err()
);
producer.write(frame(30_000, false)).unwrap();
producer.cut(Some(Timestamp::from_micros(35_000).unwrap())).unwrap();
let mut timestamps = Vec::new();
while let Some(frame) = group.read_frame().await.unwrap() {
timestamps.push(
hang::container::Frame::decode(frame.payload)
.unwrap()
.timestamp
.as_micros(),
);
}
assert_eq!(timestamps, vec![20_000, 30_000, 35_000]);
}
#[tokio::test]
async fn a_rewound_keyframe_is_refused() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video));
producer.write(frame(20_000, true)).unwrap();
producer.write(frame(30_000, false)).unwrap();
let err = producer.write(frame(0, true)).unwrap_err();
assert!(matches!(err, crate::Error::TimestampRewind(_)));
}
#[tokio::test]
async fn a_group_below_the_live_edge_is_refused() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video));
producer.write(frame(0, true)).unwrap();
producer.write(frame(33_000, false)).unwrap();
producer.cut(None).unwrap();
let err = producer.write(frame(16_000, true)).unwrap_err();
assert!(matches!(err, crate::Error::TimestampRewind(_)));
}
#[tokio::test]
async fn b_frames_within_a_group_are_accepted() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video));
for (index, timestamp) in [0, 66_000, 33_000].into_iter().enumerate() {
producer.write(frame(timestamp, index == 0)).unwrap();
}
producer.finish().unwrap();
let groups = collect_payloads(consumer).await;
assert_eq!(groups[0][..3], [(0, 2), (66_000, 2), (33_000, 2)]);
}
#[tokio::test]
async fn open_gop_leading_pictures_above_the_previous_group_are_accepted() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video));
producer.write(frame(0, true)).unwrap();
producer.write(frame(33_000, false)).unwrap();
producer.write(frame(66_000, true)).unwrap();
producer.write(frame(50_000, false)).unwrap();
producer.finish().unwrap();
let groups = collect_payloads(consumer).await;
assert_eq!(groups[1][0], (66_000, 2));
assert_eq!(groups[1][1], (50_000, 2));
}
#[tokio::test]
async fn duration_marker_resets_after_discontinuity() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let consumer = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video));
producer.write(frame(1_000_000, true)).unwrap();
producer.write(frame(1_010_000, false)).unwrap();
producer.discontinuity().unwrap();
producer.write(frame(1_020_000, true)).unwrap();
producer.write(frame(1_040_000, false)).unwrap();
producer.finish().unwrap();
let groups = collect_payloads(consumer).await;
assert_eq!(groups.last().unwrap().last(), Some(&(1_060_000, 0)));
}
#[tokio::test]
async fn a_prompt_resume_after_a_discontinuity_is_not_a_rewind() {
let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.video));
let subscriber = track.subscribe(replay());
let mut producer = Producer::new(track, Container::Legacy(crate::container::Kind::Video));
producer.write(frame(1_000_000, true)).unwrap();
producer.write(frame(1_040_000, false)).unwrap();
producer.discontinuity().unwrap();
producer.write(frame(1_050_000, true)).unwrap();
producer.finish().unwrap();
let mut consumer =
crate::container::Consumer::new(subscriber, Container::Legacy(crate::container::Kind::Video));
let mut timestamps = Vec::new();
while let Some(frame) = consumer.read().await.unwrap() {
timestamps.push(frame.timestamp.as_micros());
}
assert_eq!(timestamps, [1_000_000, 1_040_000, 1_050_000]);
}
}