use std::time::{Duration, Instant};
use bytes::Bytes;
use hang::catalog::{AudioCodec, VideoCodecKind};
use moq_mux::catalog::hang::Catalog;
use str0m::format::Codec;
use str0m::media::{Frequency, MediaTime, Mid, Pt};
use tokio::sync::mpsc;
use crate::{Error, Result, codec};
pub struct WriteRequest {
pub mid: Mid,
pub pt: Pt,
pub time: MediaTime,
pub payload: Bytes,
}
#[derive(Default)]
pub(crate) struct EgressClock {
anchor: Option<(Duration, Instant)>,
}
impl EgressClock {
pub(crate) fn wallclock(&mut self, time: MediaTime, now: Instant) -> Instant {
let presentation = Duration::from(time);
let Some((anchor_presentation, anchor_wallclock)) = self.anchor else {
self.anchor = Some((presentation, now));
return now;
};
if presentation >= anchor_presentation {
let delta = presentation - anchor_presentation;
let Some(mapped) = anchor_wallclock.checked_add(delta) else {
self.anchor = Some((presentation, now));
return now;
};
if mapped > now {
self.anchor = Some((presentation, now));
now
} else {
mapped
}
} else {
anchor_wallclock
.checked_sub(anchor_presentation - presentation)
.unwrap_or(now)
}
}
}
pub struct EgressSource {
source: moq_mux::Source,
catalog: Catalog,
writes_tx: mpsc::Sender<WriteRequest>,
writes_rx: Option<mpsc::Receiver<WriteRequest>>,
}
impl EgressSource {
pub async fn new(source: moq_mux::Source) -> Result<Self> {
let catalog_track = source
.broadcast()
.await?
.track(hang::Catalog::DEFAULT_NAME)?
.subscribe(hang::Catalog::default_subscription())
.await?;
let mut consumer = moq_mux::catalog::hang::Consumer::new(catalog_track);
let catalog = consumer
.next()
.await
.map_err(|err| Error::Other(anyhow::anyhow!("catalog subscribe: {err}")))?
.ok_or_else(|| Error::Other(anyhow::anyhow!("catalog closed before first snapshot")))?;
let (tx, rx) = mpsc::channel(64);
Ok(Self {
source,
catalog,
writes_tx: tx,
writes_rx: Some(rx),
})
}
pub fn take_writes(&mut self) -> mpsc::Receiver<WriteRequest> {
self.writes_rx.take().expect("EgressSource writes_rx already taken")
}
pub fn on_track(&mut self, mid: Mid, codec: Codec, pt: Pt, clock_rate: Frequency) -> Result<()> {
let tx = self.writes_tx.clone();
let source = self.source.clone();
let catalog = self.catalog.clone();
tokio::spawn(async move {
let track = match pick_track(&source, &catalog, codec).await {
Ok(Some(t)) => t,
Ok(None) => {
tracing::warn!(?codec, "no matching catalog rendition; egress track ignored");
return;
}
Err(err) => {
tracing::warn!(?codec, %err, "egress track subscribe failed");
return;
}
};
pump(mid, pt, clock_rate, track, tx).await;
});
Ok(())
}
pub fn catalog_codecs(&self) -> Vec<Codec> {
let mut out = Vec::new();
if self
.catalog
.audio
.renditions
.values()
.any(|r| matches!(r.codec, AudioCodec::Opus) && valid_reference(&self.source, r.broadcast.as_ref()))
{
out.push(Codec::Opus);
}
for rendition in self.catalog.video.renditions.values() {
if !valid_reference(&self.source, rendition.broadcast.as_ref()) {
continue;
}
let codec = match rendition.codec.kind() {
VideoCodecKind::H264 => Some(Codec::H264),
VideoCodecKind::H265 => Some(Codec::H265),
VideoCodecKind::VP8 => Some(Codec::Vp8),
VideoCodecKind::VP9 => Some(Codec::Vp9),
VideoCodecKind::AV1 => Some(Codec::Av1),
_ => None,
};
if let Some(c) = codec
&& !out.contains(&c)
{
out.push(c);
}
}
out
}
}
fn valid_reference(source: &moq_mux::Source, broadcast: Option<&moq_net::PathRelative<'_>>) -> bool {
source.resolve_reference(broadcast).is_some()
}
async fn pick_track(source: &moq_mux::Source, catalog: &Catalog, codec: Codec) -> Result<Option<codec::Track>> {
match codec {
Codec::Opus => {
let Some((name, config)) =
catalog.audio.renditions.iter().find(|(_, c)| {
matches!(c.codec, AudioCodec::Opus) && valid_reference(source, c.broadcast.as_ref())
})
else {
return Ok(None);
};
let track = source.subscribe_track(config.broadcast.as_ref(), name).await?;
Ok(Some(codec::Track::opus(track)))
}
Codec::H264 | Codec::H265 | Codec::Vp8 | Codec::Vp9 | Codec::Av1 => {
let target = match codec {
Codec::H264 => VideoCodecKind::H264,
Codec::H265 => VideoCodecKind::H265,
Codec::Vp8 => VideoCodecKind::VP8,
Codec::Vp9 => VideoCodecKind::VP9,
Codec::Av1 => VideoCodecKind::AV1,
_ => unreachable!(),
};
let Some((name, config)) = catalog
.video
.renditions
.iter()
.find(|(_, c)| c.codec.kind() == target && valid_reference(source, c.broadcast.as_ref()))
else {
return Ok(None);
};
let track = source.subscribe_track(config.broadcast.as_ref(), name).await?;
Ok(Some(codec::Track::video(track, config)?))
}
other => Err(Error::UnsupportedCodec(format!("{other:?}"))),
}
}
async fn pump(mid: Mid, pt: Pt, clock_rate: Frequency, mut track: codec::Track, tx: mpsc::Sender<WriteRequest>) {
loop {
let frame = match track.next().await {
Ok(Some(f)) => f,
Ok(None) => {
tracing::debug!(?mid, "egress track ended");
return;
}
Err(err) => {
tracing::warn!(?mid, %err, "egress track error");
return;
}
};
let ticks = us_to_ticks(frame.timestamp_us, clock_rate);
let time = MediaTime::new(ticks, clock_rate);
let req = WriteRequest {
mid,
pt,
time,
payload: frame.payload,
};
if tx.send(req).await.is_err() {
return;
}
}
}
fn us_to_ticks(timestamp_us: u64, clock_rate: Frequency) -> u64 {
let rate = clock_rate.get() as u128;
((timestamp_us as u128 * rate) / 1_000_000) as u64
}
pub fn dispatch(rtc: &mut str0m::Rtc, request: WriteRequest, wallclock: Instant) {
let Some(writer) = rtc.writer(request.mid) else {
tracing::debug!(?request.mid, "egress write before media available");
return;
};
let WriteRequest {
pt,
time,
payload,
mid: _,
} = request;
if let Err(err) = writer.write(pt, wallclock, time, payload.to_vec()) {
tracing::warn!(%err, "egress write rejected by str0m");
}
}
#[cfg(test)]
mod tests {
use super::*;
use hang::catalog::{AudioConfig, H264, VideoCodec, VideoConfig};
use moq_net::{Origin, PathRelative};
#[test]
fn catalog_codecs_ignores_codecs_available_only_via_escaping_references() {
let origin = Origin::random().produce();
let source = moq_mux::Source::new(origin.consume(), "a/pub");
let mut catalog = Catalog::default();
let mut escaped_audio = AudioConfig::new(AudioCodec::Opus, 48_000, 2);
escaped_audio.broadcast = Some(PathRelative::new("../../source").to_owned());
catalog.audio.renditions.insert("opus".to_string(), escaped_audio);
let mut escaped_video = VideoConfig::new(H264 {
profile: 0x42,
constraints: 0,
level: 0x1e,
inline: false,
});
escaped_video.broadcast = Some(PathRelative::new("../../source").to_owned());
catalog.video.renditions.insert("h264".to_string(), escaped_video);
let mut valid_video = VideoConfig::new(VideoCodec::VP8);
valid_video.broadcast = Some(PathRelative::new("./source").to_owned());
catalog.video.renditions.insert("vp8".to_string(), valid_video);
let (writes_tx, writes_rx) = mpsc::channel(1);
let egress = EgressSource {
source,
catalog,
writes_tx,
writes_rx: Some(writes_rx),
};
assert_eq!(egress.catalog_codecs(), vec![Codec::Vp8]);
}
#[test]
fn egress_clock_ignores_cross_track_dequeue_jitter() {
let mut clock = EgressClock::default();
let t0 = Instant::now();
assert_eq!(clock.wallclock(MediaTime::from_millis(1_000), t0), t0);
assert_eq!(
clock.wallclock(MediaTime::from_millis(1_100), t0 + Duration::from_millis(100)),
t0 + Duration::from_millis(100)
);
let audio = clock.wallclock(MediaTime::from_millis(1_200), t0 + Duration::from_millis(250));
let video = clock.wallclock(MediaTime::from_millis(1_200), t0 + Duration::from_millis(300));
assert_eq!(audio, t0 + Duration::from_millis(200));
assert_eq!(video, audio);
}
#[test]
fn egress_clock_moves_epoch_earlier_for_catch_up_bursts() {
let mut clock = EgressClock::default();
let t0 = Instant::now();
assert_eq!(clock.wallclock(MediaTime::from_millis(1_000), t0), t0);
assert_eq!(clock.wallclock(MediaTime::from_millis(1_100), t0), t0);
assert_eq!(
clock.wallclock(MediaTime::from_millis(1_100), t0 + Duration::from_millis(50)),
t0
);
}
}