use std::time::Instant;
use bytes::Bytes;
use hang::catalog::{AudioCodec, VideoCodec};
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,
}
pub struct EgressSource {
broadcast: moq_net::BroadcastConsumer,
catalog: Catalog,
writes_tx: mpsc::Sender<WriteRequest>,
writes_rx: Option<mpsc::Receiver<WriteRequest>>,
}
impl EgressSource {
pub async fn new(broadcast: moq_net::BroadcastConsumer) -> Result<Self> {
let catalog_track = broadcast.subscribe_track(&moq_net::Track::new(hang::Catalog::DEFAULT_NAME))?;
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 {
broadcast,
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 broadcast = self.broadcast.clone();
let catalog = self.catalog.clone();
tokio::spawn(async move {
let track = match pick_track(&broadcast, &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))
{
out.push(Codec::Opus);
}
for rendition in self.catalog.video.renditions.values() {
if let Some(c) = video_codec(&rendition.codec)
&& !out.contains(&c)
{
out.push(c);
}
}
out
}
}
fn video_codec(codec: &VideoCodec) -> Option<Codec> {
match codec {
VideoCodec::H264(_) => Some(Codec::H264),
VideoCodec::H265(_) => Some(Codec::H265),
VideoCodec::VP8 => Some(Codec::Vp8),
VideoCodec::VP9(_) => Some(Codec::Vp9),
VideoCodec::AV1(_) => Some(Codec::Av1),
_ => None,
}
}
async fn pick_track(
broadcast: &moq_net::BroadcastConsumer,
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))
else {
return Ok(None);
};
Ok(Some(codec::Track::opus(broadcast, name).await?))
}
Codec::H264 | Codec::H265 | Codec::Vp8 | Codec::Vp9 | Codec::Av1 => {
let Some((name, config)) = catalog
.video
.renditions
.iter()
.find(|(_, c)| video_codec(&c.codec) == Some(codec))
else {
return Ok(None);
};
Ok(Some(codec::Track::video(broadcast, name, config).await?))
}
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");
}
}