1use std::time::Instant;
15
16use bytes::Bytes;
17use hang::catalog::{AudioCodec, VideoCodec};
18use moq_mux::catalog::hang::Catalog;
19use str0m::format::Codec;
20use str0m::media::{Frequency, MediaTime, Mid, Pt};
21use tokio::sync::mpsc;
22
23use crate::{Error, Result, codec};
24
25pub struct WriteRequest {
30 pub mid: Mid,
31 pub pt: Pt,
32 pub time: MediaTime,
33 pub payload: Bytes,
34}
35
36pub struct EgressSource {
38 broadcast: moq_net::BroadcastConsumer,
39 catalog: Catalog,
43 writes_tx: mpsc::Sender<WriteRequest>,
44 writes_rx: Option<mpsc::Receiver<WriteRequest>>,
45}
46
47impl EgressSource {
48 pub async fn new(broadcast: moq_net::BroadcastConsumer) -> Result<Self> {
54 let catalog_track = broadcast.subscribe_track(&moq_net::Track::new(hang::Catalog::DEFAULT_NAME))?;
55 let mut consumer = moq_mux::catalog::hang::Consumer::new(catalog_track);
56 let catalog = consumer
57 .next()
58 .await
59 .map_err(|err| Error::Other(anyhow::anyhow!("catalog subscribe: {err}")))?
60 .ok_or_else(|| Error::Other(anyhow::anyhow!("catalog closed before first snapshot")))?;
61
62 let (tx, rx) = mpsc::channel(64);
63 Ok(Self {
64 broadcast,
65 catalog,
66 writes_tx: tx,
67 writes_rx: Some(rx),
68 })
69 }
70
71 pub fn take_writes(&mut self) -> mpsc::Receiver<WriteRequest> {
74 self.writes_rx.take().expect("EgressSource writes_rx already taken")
75 }
76
77 pub fn on_track(&mut self, mid: Mid, codec: Codec, pt: Pt, clock_rate: Frequency) -> Result<()> {
83 let tx = self.writes_tx.clone();
86 let broadcast = self.broadcast.clone();
87 let catalog = self.catalog.clone();
88 tokio::spawn(async move {
89 let track = match pick_track(&broadcast, &catalog, codec).await {
90 Ok(Some(t)) => t,
91 Ok(None) => {
92 tracing::warn!(?codec, "no matching catalog rendition; egress track ignored");
93 return;
94 }
95 Err(err) => {
96 tracing::warn!(?codec, %err, "egress track subscribe failed");
97 return;
98 }
99 };
100 pump(mid, pt, clock_rate, track, tx).await;
101 });
102 Ok(())
103 }
104
105 pub fn catalog_codecs(&self) -> Vec<Codec> {
109 let mut out = Vec::new();
110 if self
111 .catalog
112 .audio
113 .renditions
114 .values()
115 .any(|r| matches!(r.codec, AudioCodec::Opus))
116 {
117 out.push(Codec::Opus);
118 }
119 for rendition in self.catalog.video.renditions.values() {
120 if let Some(c) = video_codec(&rendition.codec)
121 && !out.contains(&c)
122 {
123 out.push(c);
124 }
125 }
126 out
127 }
128}
129
130fn video_codec(codec: &VideoCodec) -> Option<Codec> {
132 match codec {
133 VideoCodec::H264(_) => Some(Codec::H264),
134 VideoCodec::H265(_) => Some(Codec::H265),
135 VideoCodec::VP8 => Some(Codec::Vp8),
136 VideoCodec::VP9(_) => Some(Codec::Vp9),
137 VideoCodec::AV1(_) => Some(Codec::Av1),
138 _ => None,
139 }
140}
141
142async fn pick_track(
145 broadcast: &moq_net::BroadcastConsumer,
146 catalog: &Catalog,
147 codec: Codec,
148) -> Result<Option<codec::Track>> {
149 match codec {
150 Codec::Opus => {
151 let Some((name, _config)) = catalog
152 .audio
153 .renditions
154 .iter()
155 .find(|(_, c)| matches!(c.codec, AudioCodec::Opus))
156 else {
157 return Ok(None);
158 };
159 Ok(Some(codec::Track::opus(broadcast, name).await?))
160 }
161 Codec::H264 | Codec::H265 | Codec::Vp8 | Codec::Vp9 | Codec::Av1 => {
162 let Some((name, config)) = catalog
163 .video
164 .renditions
165 .iter()
166 .find(|(_, c)| video_codec(&c.codec) == Some(codec))
167 else {
168 return Ok(None);
169 };
170 Ok(Some(codec::Track::video(broadcast, name, config).await?))
171 }
172 other => Err(Error::UnsupportedCodec(format!("{other:?}"))),
173 }
174}
175
176async fn pump(mid: Mid, pt: Pt, clock_rate: Frequency, mut track: codec::Track, tx: mpsc::Sender<WriteRequest>) {
179 loop {
180 let frame = match track.next().await {
181 Ok(Some(f)) => f,
182 Ok(None) => {
183 tracing::debug!(?mid, "egress track ended");
184 return;
185 }
186 Err(err) => {
187 tracing::warn!(?mid, %err, "egress track error");
188 return;
189 }
190 };
191 let ticks = us_to_ticks(frame.timestamp_us, clock_rate);
192 let time = MediaTime::new(ticks, clock_rate);
193 let req = WriteRequest {
194 mid,
195 pt,
196 time,
197 payload: frame.payload,
198 };
199 if tx.send(req).await.is_err() {
200 return;
202 }
203 }
204}
205
206fn us_to_ticks(timestamp_us: u64, clock_rate: Frequency) -> u64 {
209 let rate = clock_rate.get() as u128;
210 ((timestamp_us as u128 * rate) / 1_000_000) as u64
211}
212
213pub fn dispatch(rtc: &mut str0m::Rtc, request: WriteRequest, wallclock: Instant) {
220 let Some(writer) = rtc.writer(request.mid) else {
221 tracing::debug!(?request.mid, "egress write before media available");
222 return;
223 };
224 let WriteRequest {
225 pt,
226 time,
227 payload,
228 mid: _,
229 } = request;
230 if let Err(err) = writer.write(pt, wallclock, time, payload.to_vec()) {
231 tracing::warn!(%err, "egress write rejected by str0m");
232 }
233}