1use std::time::{Duration, Instant};
15
16use bytes::Bytes;
17use hang::catalog::{AudioCodec, VideoCodecKind};
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,
32 pub pt: Pt,
34 pub time: MediaTime,
36 pub payload: Bytes,
38}
39
40#[derive(Default)]
48pub(crate) struct EgressClock {
49 anchor: Option<(Duration, Instant)>,
50}
51
52impl EgressClock {
53 pub(crate) fn wallclock(&mut self, time: MediaTime, now: Instant) -> Instant {
55 let presentation = Duration::from(time);
56 let Some((anchor_presentation, anchor_wallclock)) = self.anchor else {
57 self.anchor = Some((presentation, now));
58 return now;
59 };
60
61 if presentation >= anchor_presentation {
62 let delta = presentation - anchor_presentation;
63 let Some(mapped) = anchor_wallclock.checked_add(delta) else {
64 self.anchor = Some((presentation, now));
65 return now;
66 };
67 if mapped > now {
68 self.anchor = Some((presentation, now));
72 now
73 } else {
74 mapped
75 }
76 } else {
77 anchor_wallclock
78 .checked_sub(anchor_presentation - presentation)
79 .unwrap_or(now)
80 }
81 }
82}
83
84pub struct EgressSource {
86 source: moq_mux::Source,
89 catalog: Catalog,
93 writes_tx: mpsc::Sender<WriteRequest>,
94 writes_rx: Option<mpsc::Receiver<WriteRequest>>,
95}
96
97impl EgressSource {
98 pub async fn new(source: moq_mux::Source) -> Result<Self> {
108 let catalog_track = source
109 .broadcast()
110 .await?
111 .track(hang::Catalog::DEFAULT_NAME)?
112 .subscribe(hang::Catalog::default_subscription())
113 .await?;
114 let mut consumer = moq_mux::catalog::hang::Consumer::new(catalog_track);
115 let catalog = consumer
116 .next()
117 .await
118 .map_err(|err| Error::Other(anyhow::anyhow!("catalog subscribe: {err}")))?
119 .ok_or_else(|| Error::Other(anyhow::anyhow!("catalog closed before first snapshot")))?;
120
121 let (tx, rx) = mpsc::channel(64);
122 Ok(Self {
123 source,
124 catalog,
125 writes_tx: tx,
126 writes_rx: Some(rx),
127 })
128 }
129
130 pub fn take_writes(&mut self) -> mpsc::Receiver<WriteRequest> {
133 self.writes_rx.take().expect("EgressSource writes_rx already taken")
134 }
135
136 pub fn on_track(&mut self, mid: Mid, codec: Codec, pt: Pt, clock_rate: Frequency) -> Result<()> {
142 let tx = self.writes_tx.clone();
145 let source = self.source.clone();
146 let catalog = self.catalog.clone();
147 tokio::spawn(async move {
148 let track = match pick_track(&source, &catalog, codec).await {
149 Ok(Some(t)) => t,
150 Ok(None) => {
151 tracing::warn!(?codec, "no matching catalog rendition; egress track ignored");
152 return;
153 }
154 Err(err) => {
155 tracing::warn!(?codec, %err, "egress track subscribe failed");
156 return;
157 }
158 };
159 pump(mid, pt, clock_rate, track, tx).await;
160 });
161 Ok(())
162 }
163
164 pub fn catalog_codecs(&self) -> Vec<Codec> {
168 let mut out = Vec::new();
169 if self
170 .catalog
171 .audio
172 .renditions
173 .values()
174 .any(|r| matches!(r.codec, AudioCodec::Opus) && valid_reference(&self.source, r.broadcast.as_ref()))
175 {
176 out.push(Codec::Opus);
177 }
178 for rendition in self.catalog.video.renditions.values() {
179 if !valid_reference(&self.source, rendition.broadcast.as_ref()) {
180 continue;
181 }
182 let codec = match rendition.codec.kind() {
183 VideoCodecKind::H264 => Some(Codec::H264),
184 VideoCodecKind::H265 => Some(Codec::H265),
185 VideoCodecKind::VP8 => Some(Codec::Vp8),
186 VideoCodecKind::VP9 => Some(Codec::Vp9),
187 VideoCodecKind::AV1 => Some(Codec::Av1),
188 _ => None,
189 };
190 if let Some(c) = codec
191 && !out.contains(&c)
192 {
193 out.push(c);
194 }
195 }
196 out
197 }
198}
199
200fn valid_reference(source: &moq_mux::Source, broadcast: Option<&moq_net::PathRelative<'_>>) -> bool {
201 source.resolve_reference(broadcast).is_some()
202}
203
204async fn pick_track(source: &moq_mux::Source, catalog: &Catalog, codec: Codec) -> Result<Option<codec::Track>> {
209 match codec {
210 Codec::Opus => {
211 let Some((name, config)) =
212 catalog.audio.renditions.iter().find(|(_, c)| {
213 matches!(c.codec, AudioCodec::Opus) && valid_reference(source, c.broadcast.as_ref())
214 })
215 else {
216 return Ok(None);
217 };
218 let track = source.subscribe_track(config.broadcast.as_ref(), name).await?;
219 Ok(Some(codec::Track::opus(track)))
220 }
221 Codec::H264 | Codec::H265 | Codec::Vp8 | Codec::Vp9 | Codec::Av1 => {
222 let target = match codec {
223 Codec::H264 => VideoCodecKind::H264,
224 Codec::H265 => VideoCodecKind::H265,
225 Codec::Vp8 => VideoCodecKind::VP8,
226 Codec::Vp9 => VideoCodecKind::VP9,
227 Codec::Av1 => VideoCodecKind::AV1,
228 _ => unreachable!(),
229 };
230 let Some((name, config)) = catalog
231 .video
232 .renditions
233 .iter()
234 .find(|(_, c)| c.codec.kind() == target && valid_reference(source, c.broadcast.as_ref()))
235 else {
236 return Ok(None);
237 };
238 let track = source.subscribe_track(config.broadcast.as_ref(), name).await?;
239 Ok(Some(codec::Track::video(track, config)?))
240 }
241 other => Err(Error::UnsupportedCodec(format!("{other:?}"))),
242 }
243}
244
245async fn pump(mid: Mid, pt: Pt, clock_rate: Frequency, mut track: codec::Track, tx: mpsc::Sender<WriteRequest>) {
248 loop {
249 let frame = match track.next().await {
250 Ok(Some(f)) => f,
251 Ok(None) => {
252 tracing::debug!(?mid, "egress track ended");
253 return;
254 }
255 Err(err) => {
256 tracing::warn!(?mid, %err, "egress track error");
257 return;
258 }
259 };
260 let ticks = us_to_ticks(frame.timestamp_us, clock_rate);
261 let time = MediaTime::new(ticks, clock_rate);
262 let req = WriteRequest {
263 mid,
264 pt,
265 time,
266 payload: frame.payload,
267 };
268 if tx.send(req).await.is_err() {
269 return;
271 }
272 }
273}
274
275fn us_to_ticks(timestamp_us: u64, clock_rate: Frequency) -> u64 {
278 let rate = clock_rate.get() as u128;
279 ((timestamp_us as u128 * rate) / 1_000_000) as u64
280}
281
282pub fn dispatch(rtc: &mut str0m::Rtc, request: WriteRequest, wallclock: Instant) {
289 let Some(writer) = rtc.writer(request.mid) else {
290 tracing::debug!(?request.mid, "egress write before media available");
291 return;
292 };
293 let WriteRequest {
294 pt,
295 time,
296 payload,
297 mid: _,
298 } = request;
299 if let Err(err) = writer.write(pt, wallclock, time, payload.to_vec()) {
300 tracing::warn!(%err, "egress write rejected by str0m");
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use hang::catalog::{AudioConfig, H264, VideoCodec, VideoConfig};
308 use moq_net::{Origin, PathRelative};
309
310 #[test]
311 fn catalog_codecs_ignores_codecs_available_only_via_escaping_references() {
312 let origin = Origin::random().produce();
313 let source = moq_mux::Source::new(origin.consume(), "a/pub");
314 let mut catalog = Catalog::default();
315
316 let mut escaped_audio = AudioConfig::new(AudioCodec::Opus, 48_000, 2);
317 escaped_audio.broadcast = Some(PathRelative::new("../../source").to_owned());
318 catalog.audio.renditions.insert("opus".to_string(), escaped_audio);
319
320 let mut escaped_video = VideoConfig::new(H264 {
321 profile: 0x42,
322 constraints: 0,
323 level: 0x1e,
324 inline: false,
325 });
326 escaped_video.broadcast = Some(PathRelative::new("../../source").to_owned());
327 catalog.video.renditions.insert("h264".to_string(), escaped_video);
328
329 let mut valid_video = VideoConfig::new(VideoCodec::VP8);
330 valid_video.broadcast = Some(PathRelative::new("./source").to_owned());
331 catalog.video.renditions.insert("vp8".to_string(), valid_video);
332
333 let (writes_tx, writes_rx) = mpsc::channel(1);
334 let egress = EgressSource {
335 source,
336 catalog,
337 writes_tx,
338 writes_rx: Some(writes_rx),
339 };
340
341 assert_eq!(egress.catalog_codecs(), vec![Codec::Vp8]);
342 }
343
344 #[test]
345 fn egress_clock_ignores_cross_track_dequeue_jitter() {
346 let mut clock = EgressClock::default();
347 let t0 = Instant::now();
348
349 assert_eq!(clock.wallclock(MediaTime::from_millis(1_000), t0), t0);
350 assert_eq!(
351 clock.wallclock(MediaTime::from_millis(1_100), t0 + Duration::from_millis(100)),
352 t0 + Duration::from_millis(100)
353 );
354
355 let audio = clock.wallclock(MediaTime::from_millis(1_200), t0 + Duration::from_millis(250));
358 let video = clock.wallclock(MediaTime::from_millis(1_200), t0 + Duration::from_millis(300));
359 assert_eq!(audio, t0 + Duration::from_millis(200));
360 assert_eq!(video, audio);
361 }
362
363 #[test]
364 fn egress_clock_moves_epoch_earlier_for_catch_up_bursts() {
365 let mut clock = EgressClock::default();
366 let t0 = Instant::now();
367
368 assert_eq!(clock.wallclock(MediaTime::from_millis(1_000), t0), t0);
369 assert_eq!(clock.wallclock(MediaTime::from_millis(1_100), t0), t0);
372
373 assert_eq!(
376 clock.wallclock(MediaTime::from_millis(1_100), t0 + Duration::from_millis(50)),
377 t0
378 );
379 }
380}