use std::sync::{Arc, Mutex};
use std::task::Poll;
use std::time::SystemTime;
use hang::catalog::Timeline;
use hang::timeline::{Record, RecordExt, track_name};
use moq_net::{Timescale, Timestamp};
pub const DEFAULT_GRANULARITY: Timestamp = Timestamp::new_const(1, Timescale::SECOND);
#[derive(Clone)]
pub struct Producer {
inner: moq_json::stream::Producer<Record>,
track: String,
timescale: Timescale,
granularity: Timestamp,
wall: Arc<Mutex<Option<u64>>>,
}
impl Producer {
pub fn new(broadcast: &mut moq_net::broadcast::Producer, name: &str) -> Result<Self, moq_net::Error> {
let track = track_name(name);
let net = broadcast.create_track(track.as_str(), None)?;
let config = moq_json::stream::ProducerConfig::default().with_compression(true);
Ok(Self {
inner: moq_json::stream::Producer::new(net, config),
track,
timescale: Timescale::new(Timeline::default_timescale() as u64).expect("default timescale is nonzero"),
granularity: DEFAULT_GRANULARITY,
wall: Arc::new(Mutex::new(None)),
})
}
pub fn with_granularity(mut self, granularity: Timestamp) -> Self {
self.granularity = granularity;
self
}
pub fn section(&self) -> Timeline {
let mut section = Timeline::new(&self.track);
section.timescale = self.timescale.as_u64() as u32;
section.wall = *self.wall.lock().unwrap();
section
}
pub fn set_wall(&mut self, pts: Timestamp, wall: SystemTime) {
let unix_millis = wall
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let scale = self.timescale.as_u64() as u128;
let pts_units = pts.as_scale(self.timescale);
let moq_millis = unix_millis.saturating_sub(hang::catalog::MOQ_EPOCH_UNIX_MILLIS as u128);
let moq_units = moq_millis * scale / 1000;
*self.wall.lock().unwrap() = Some(moq_units.saturating_sub(pts_units) as u64);
}
pub fn recorder(&self) -> Recorder {
Recorder {
inner: self.inner.clone(),
timescale: self.timescale,
granularity: self.granularity,
last: None,
}
}
pub fn finish(&mut self) -> Result<(), moq_net::Error> {
match self.inner.finish() {
Ok(()) => Ok(()),
Err(moq_json::Error::Net(err)) => Err(err),
Err(err) => unreachable!("timeline finish failed to encode: {err}"),
}
}
}
pub struct Recorder {
inner: moq_json::stream::Producer<Record>,
timescale: Timescale,
granularity: Timestamp,
last: Option<Timestamp>,
}
impl Recorder {
pub(crate) fn record(&mut self, sequence: u64, pts: Timestamp) -> Result<(), moq_net::Error> {
if let Some(last) = self.last
&& pts.as_micros() < last.as_micros() + self.granularity.as_micros()
{
return Ok(());
}
self.last = Some(pts);
let record = Record::new(sequence, pts.as_scale(self.timescale) as u64);
match self.inner.append(&record) {
Ok(()) => Ok(()),
Err(moq_json::Error::Net(err)) => Err(err),
Err(err) => unreachable!("timeline record failed to encode: {err}"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Entry<E: RecordExt = ()> {
pub group: u64,
pub pts: Timestamp,
pub ext: E,
}
pub struct Consumer<E: RecordExt = ()> {
inner: moq_json::stream::Consumer<Record<E>>,
timescale: Timescale,
}
impl<E: RecordExt> Consumer<E> {
pub async fn subscribe(broadcast: &moq_net::broadcast::Consumer, section: &Timeline) -> crate::Result<Self> {
let track = broadcast.track(§ion.track)?.subscribe(None).await?;
let config = moq_json::stream::ConsumerConfig::default().with_compression(true);
Ok(Self {
inner: moq_json::stream::Consumer::new(track, config),
timescale: Timescale::new(section.timescale as u64)
.map_err(|_| crate::Error::InvalidTimescale(section.timescale))?,
})
}
fn decode(&self, record: Record<E>) -> crate::Result<Entry<E>> {
Ok(Entry {
group: record.group,
pts: Timestamp::new(record.pts, self.timescale)?,
ext: record.ext,
})
}
pub async fn next(&mut self) -> crate::Result<Option<Entry<E>>> {
match self.inner.next().await? {
Some(record) => Ok(Some(self.decode(record)?)),
None => Ok(None),
}
}
pub fn poll_next(&mut self, waiter: &kio::Waiter) -> Poll<crate::Result<Option<Entry<E>>>> {
match self.inner.poll_next(waiter)? {
Poll::Ready(Some(record)) => Poll::Ready(self.decode(record).map(Some)),
Poll::Ready(None) => Poll::Ready(Ok(None)),
Poll::Pending => Poll::Pending,
}
}
}
#[cfg(test)]
mod test {
use std::time::Duration;
use super::*;
fn entry(group: u64, pts_ms: u64) -> Entry {
Entry {
group,
pts: Timestamp::from_millis(pts_ms).unwrap(),
ext: (),
}
}
async fn drain(broadcast: &moq_net::broadcast::Producer, producer: &Producer) -> Vec<Entry> {
let mut consumer = Consumer::subscribe(&broadcast.consume(), &producer.section())
.await
.unwrap();
let waiter = kio::Waiter::noop();
let mut out = Vec::new();
while let Poll::Ready(Ok(Some(entry))) = consumer.poll_next(&waiter) {
out.push(entry);
}
out
}
fn frame(timestamp_us: u64, keyframe: bool) -> crate::container::Frame {
crate::container::Frame {
timestamp: Timestamp::from_micros(timestamp_us).unwrap(),
payload: bytes::Bytes::from_static(&[0xDE, 0xAD]),
keyframe,
duration: None,
}
}
#[tokio::test]
async fn records_group_opens_in_milliseconds() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let mut timeline = Producer::new(&mut broadcast, "video0").unwrap();
assert_eq!(timeline.track, "video0.timeline.z");
let track = broadcast.create_track("video0", None).unwrap();
let mut media = crate::container::Producer::new(track, crate::catalog::hang::Container::Legacy)
.with_recorder(timeline.recorder());
media.write(frame(0, true)).unwrap(); media.write(frame(2_000_000, false)).unwrap(); media.write(frame(4_000_000, true)).unwrap(); media.finish().unwrap();
timeline.finish().unwrap();
assert_eq!(drain(&broadcast, &timeline).await, vec![entry(0, 0), entry(1, 4_000)]);
}
#[tokio::test]
async fn granularity_throttles_records() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let mut timeline = Producer::new(&mut broadcast, "audio0").unwrap();
let mut recorder = timeline.recorder();
for (seq, ms) in [(0u64, 0u64), (1, 300), (2, 600), (3, 900), (4, 1200)] {
recorder.record(seq, Timestamp::from_millis(ms).unwrap()).unwrap();
}
drop(recorder);
timeline.finish().unwrap();
assert_eq!(drain(&broadcast, &timeline).await, vec![entry(0, 0), entry(4, 1200)]);
}
#[test]
fn section_advertises_track_and_wall() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let mut timeline = Producer::new(&mut broadcast, "audio0").unwrap();
let section = timeline.section();
assert_eq!(section.track, "audio0.timeline.z");
assert_eq!(section.timescale, 1000);
assert_eq!(section.wall, None);
let moq = hang::catalog::MOQ_EPOCH_UNIX_MILLIS;
let observed = SystemTime::UNIX_EPOCH + Duration::from_millis(1_751_846_400_000);
timeline.set_wall(Timestamp::from_micros(0).unwrap(), observed);
assert_eq!(timeline.section().wall, Some(1_751_846_400_000 - moq));
timeline.set_wall(Timestamp::from_micros(2_000_000).unwrap(), observed);
assert_eq!(timeline.section().wall, Some(1_751_846_400_000 - moq - 2_000));
}
#[tokio::test]
async fn rejects_an_invalid_timescale() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let mut timeline = Producer::new(&mut broadcast, "video0").unwrap();
timeline.finish().unwrap();
let mut section = timeline.section();
section.timescale = 0;
match Consumer::<()>::subscribe(&broadcast.consume(), §ion).await {
Err(crate::Error::InvalidTimescale(0)) => {}
Err(err) => panic!("expected an invalid timescale, got {err:?}"),
Ok(_) => panic!("expected an invalid timescale to be rejected"),
}
}
#[tokio::test]
async fn rejects_an_out_of_range_pts() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let timeline = Producer::new(&mut broadcast, "video0").unwrap();
let track = broadcast.create_track("raw.timeline.z", None).unwrap();
let config = moq_json::stream::ProducerConfig::default().with_compression(true);
let mut raw = moq_json::stream::Producer::new(track, config);
raw.append(&Record::<()>::new(0, u64::MAX)).unwrap();
raw.finish().unwrap();
let mut section = timeline.section();
section.track = "raw.timeline.z".to_string();
let mut consumer = Consumer::<()>::subscribe(&broadcast.consume(), §ion).await.unwrap();
let waiter = kio::Waiter::noop();
match consumer.poll_next(&waiter) {
Poll::Ready(Err(crate::Error::TimestampOverflow(_))) => {}
other => panic!("expected a decode error, got {other:?}"),
}
}
#[tokio::test]
async fn consumer_decodes_pts_from_the_section() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let mut timeline = Producer::new(&mut broadcast, "video0").unwrap();
timeline
.recorder()
.record(3, Timestamp::from_micros(7_000).unwrap())
.unwrap();
timeline.finish().unwrap();
let entries = drain(&broadcast, &timeline).await;
assert_eq!(entries, vec![entry(3, 7)]);
assert_eq!(entries[0].pts.as_micros(), 7_000);
}
}