1pub mod master;
22mod mpd;
23mod playlist;
24mod rendition;
25mod upstream;
26
27pub mod renditions;
28pub mod segments;
29
30use std::sync::{Arc, Mutex};
31use std::time::{Duration, SystemTime};
32
33use moq_mux::catalog::{self, CatalogFormat, Stream};
34use rand::RngExt;
35
36pub(crate) use playlist::render_media;
37pub use rendition::{Kind, Rendition};
38pub(crate) use upstream::Upstream;
39
40const CATALOG_RETRY_MIN: Duration = Duration::from_millis(250);
52const CATALOG_RETRY_MAX: Duration = Duration::from_secs(5);
53
54#[derive(Clone, Debug)]
59#[non_exhaustive]
60pub struct Config {
61 pub window: Duration,
65}
66
67impl Default for Config {
68 fn default() -> Self {
69 Self {
70 window: Duration::from_secs(16),
71 }
72 }
73}
74
75pub struct Broadcaster {
77 #[cfg_attr(not(feature = "server"), allow(dead_code))]
80 broadcast: moq_net::broadcast::Consumer,
81 renditions: renditions::Producer,
83 watcher: Mutex<Option<tokio::task::JoinHandle<()>>>,
87 timeline_watcher: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
91}
92
93impl Broadcaster {
94 pub async fn new(source: moq_mux::Source, config: Config) -> crate::Result<Arc<Self>> {
96 let broadcast = source.broadcast().await?;
97 let upstream = Upstream {
98 source,
99 broadcast: broadcast.clone(),
100 };
101 let renditions = renditions::Producer::new(config.window);
102 let timeline_watcher = Arc::new(Mutex::new(None));
103 let broadcaster = Arc::new(Self {
104 broadcast,
105 renditions: renditions.clone(),
106 watcher: Mutex::new(None),
107 timeline_watcher: timeline_watcher.clone(),
108 });
109 let watcher = tokio::spawn(watch_catalog(upstream, renditions, timeline_watcher));
112 *broadcaster.watcher.lock().unwrap() = Some(watcher);
113 Ok(broadcaster)
114 }
115
116 pub fn is_closed(&self) -> bool {
118 self.broadcast.is_closed()
119 }
120
121 pub async fn closed(&self) {
123 self.broadcast.closed().await;
124 }
125
126 pub fn rendition(&self, kind: Kind, name: &str) -> Option<Arc<Rendition>> {
129 self.renditions.get(kind, name)
130 }
131
132 pub fn renditions(&self) -> renditions::Consumer {
136 self.renditions.subscribe()
137 }
138
139 pub async fn ready(&self) {
142 self.renditions.ready().await;
143 }
144
145 pub fn set_generation(&self, generation: Option<&str>) -> crate::Result<()> {
156 if let Some(generation) = generation
157 && (generation.is_empty()
158 || !generation
159 .bytes()
160 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'))
161 {
162 return Err(crate::Error::InvalidGeneration(generation.to_string()));
163 }
164 self.renditions.fanout().set_generation(generation.map(Arc::from));
165 Ok(())
166 }
167
168 pub fn master_playlist(&self, query: Option<&str>) -> String {
177 let mut video = Vec::new();
178 let mut audio = Vec::new();
179 for rendition in self.renditions.snapshot() {
180 if !rendition.is_playable() {
181 continue;
182 }
183 match rendition.kind {
184 Kind::Video => video.push(master::VideoVariant {
185 uri: master::rendition_uri(Kind::Video, &rendition.name, query),
186 bandwidth: rendition.bandwidth(),
187 width: rendition.width,
188 height: rendition.height,
189 codec: rendition.codec.clone(),
190 }),
191 Kind::Audio => audio.push(master::AudioVariant {
192 name: rendition.name.clone(),
193 uri: master::rendition_uri(Kind::Audio, &rendition.name, query),
194 bandwidth: rendition.bandwidth(),
195 codec: rendition.codec.clone(),
196 }),
197 }
198 }
199 master::render(&video, &audio)
200 }
201
202 pub fn manifest(&self, query: Option<&str>) -> Option<String> {
215 let mut video = Vec::new();
216 let mut audio = Vec::new();
217 let mut availability_start = None;
218 for rendition in self.renditions.snapshot() {
219 if availability_start.is_none() {
222 availability_start = rendition.wall_clock(moq_net::Timestamp::ZERO);
223 }
224 let Some(representation) = rendition.representation() else {
227 continue;
228 };
229 match rendition.kind {
230 Kind::Video => video.push(representation),
231 Kind::Audio => audio.push(representation),
232 }
233 }
234
235 let representations = || video.iter().chain(&audio);
236 if representations().all(|rep| rep.segments.is_empty()) {
237 return None;
238 }
239 let finished = representations().any(|rep| rep.ended);
240 if !finished {
241 availability_start = availability_start.or_else(|| self.renditions.anchor());
244 availability_start?;
245 }
246
247 Some(mpd::render_manifest(
248 &mpd::Manifest {
249 availability_start,
250 publish: SystemTime::now(),
251 window: self.renditions.window(),
252 finished,
253 video,
254 audio,
255 },
256 query,
257 ))
258 }
259
260 #[cfg(feature = "server")]
263 pub(crate) async fn build_inits(&self) {
264 for rendition in self.renditions.snapshot() {
265 if let Err(err) = rendition.init().await {
266 tracing::warn!(rendition = %rendition.name, %err, "failed to build init segment");
267 }
268 }
269 }
270
271 #[cfg_attr(not(feature = "server"), allow(dead_code))]
276 pub(crate) async fn playable(&self) {
277 if let Some(rendition) = self.renditions.snapshot().into_iter().next() {
278 rendition.playable().await;
279 }
280 }
281
282 #[cfg_attr(not(feature = "server"), allow(dead_code))]
284 pub(crate) fn is_empty(&self) -> bool {
285 self.renditions.is_empty()
286 }
287}
288
289impl Drop for Broadcaster {
290 fn drop(&mut self) {
291 if let Some(watcher) = self.watcher.lock().unwrap().take() {
292 watcher.abort();
293 }
294 let held = self.renditions.any_held_externally();
299 self.renditions.clear();
302 if !held && let Some(watcher) = self.timeline_watcher.lock().unwrap().take() {
303 watcher.abort();
304 }
305 }
306}
307
308async fn watch_catalog(
309 upstream: Upstream,
310 renditions: renditions::Producer,
311 timeline_watcher: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
312) {
313 let broadcast = &upstream.broadcast;
317 let mut delay = CATALOG_RETRY_MIN;
318 let mut consumer = loop {
319 match catalog::Consumer::<()>::new(broadcast, CatalogFormat::Hang).await {
320 Ok(consumer) => break consumer,
321 Err(err) => {
322 tracing::warn!(%err, "failed to subscribe to broadcast catalog, retrying");
323 let wait = delay.mul_f64(0.5 + rand::rng().random::<f64>() / 2.0);
324 delay = (delay * 2).min(CATALOG_RETRY_MAX);
325 tokio::select! {
326 _ = tokio::time::sleep(wait) => {}
327 _ = kio::wait(|waiter| broadcast.poll_closed(waiter)) => {
328 renditions.close();
329 return;
330 }
331 }
332 }
333 }
334 };
335
336 let mut timeline_started = false;
337 loop {
338 match kio::wait(|waiter| consumer.poll_next(waiter)).await {
339 Ok(Some(catalog)) => {
340 renditions.sync(&upstream, &catalog);
341 if !timeline_started && let Some(archive) = catalog.archive.clone() {
344 timeline_started = true;
345 let watcher = tokio::spawn(watch_timeline(broadcast.clone(), archive, renditions.fanout()));
346 *timeline_watcher.lock().unwrap() = Some(watcher);
347 }
348 }
349 Ok(None) => break,
350 Err(err) => {
351 tracing::warn!(%err, "broadcast catalog stream ended with error");
352 break;
353 }
354 }
355 }
356
357 renditions.close();
359}
360
361async fn watch_timeline(
364 broadcast: moq_net::broadcast::Consumer,
365 section: hang::catalog::Archive,
366 renditions: renditions::Fanout,
367) {
368 match watch(&broadcast, §ion, &renditions).await {
369 Ok(()) => renditions.end_windows(),
372 Err(err) => {
375 tracing::warn!(track = %section.track, %err, "timeline watcher error; leaving the playlists live")
376 }
377 }
378 renditions.close_windows();
381}
382
383async fn watch(
384 broadcast: &moq_net::broadcast::Consumer,
385 section: &hang::catalog::Archive,
386 renditions: &renditions::Fanout,
387) -> crate::Result<()> {
388 let mut timeline = moq_mux::timeline::Consumer::<()>::subscribe(broadcast, section).await?;
389 while let Some(event) = timeline.next().await? {
390 match event {
391 moq_mux::timeline::Event::Push { index, entry } => renditions.push(index, entry),
392 moq_mux::timeline::Event::Pop(range) => renditions.pop(range),
393 moq_mux::timeline::Event::Skip(_) => renditions.skip(),
394 _ => unreachable!("unknown timeline event"),
395 }
396 }
397 Ok(())
398}
399
400#[cfg(test)]
401mod tests {
402 fn produce_origin() -> moq_net::origin::Producer {
404 let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default());
405 if tokio::runtime::Handle::try_current().is_ok() {
406 tokio::spawn(moq_net::time::run(driver));
407 } else {
408 std::mem::forget(driver);
411 }
412 producer
413 }
414
415 use super::*;
416
417 enum TestConfig {
418 Video(hang::catalog::VideoConfig),
419 Audio(hang::catalog::AudioConfig),
420 }
421
422 impl From<hang::catalog::VideoConfig> for TestConfig {
423 fn from(config: hang::catalog::VideoConfig) -> Self {
424 Self::Video(config)
425 }
426 }
427
428 impl From<hang::catalog::AudioConfig> for TestConfig {
429 fn from(config: hang::catalog::AudioConfig) -> Self {
430 Self::Audio(config)
431 }
432 }
433
434 struct TestRendition {
435 catalog: moq_mux::catalog::Producer,
436 name: String,
437 config: Option<TestConfig>,
438 }
439
440 impl TestRendition {
441 fn unwrap(self) -> Self {
442 self
443 }
444
445 fn set(&mut self, config: impl Into<TestConfig>) -> moq_mux::Result<()> {
446 let config = config.into();
447 let mut catalog = self.catalog.modify()?;
448 match &config {
449 TestConfig::Video(config) => {
450 catalog.video.renditions.insert(self.name.clone(), config.clone());
451 }
452 TestConfig::Audio(config) => {
453 catalog.audio.renditions.insert(self.name.clone(), config.clone());
454 }
455 }
456 catalog.commit()?;
457 self.config = Some(config);
458 Ok(())
459 }
460 }
461
462 impl Drop for TestRendition {
463 fn drop(&mut self) {
464 let Ok(mut catalog) = self.catalog.modify() else {
465 return;
466 };
467 match self.config {
468 Some(TestConfig::Video(_)) => {
469 catalog.video.renditions.remove(&self.name);
470 }
471 Some(TestConfig::Audio(_)) => {
472 catalog.audio.renditions.remove(&self.name);
473 }
474 None => {}
475 }
476 }
477 }
478
479 trait RenditionTestExt {
480 fn test_video(&self, name: &str) -> TestRendition;
481 fn test_audio(&self, name: &str) -> TestRendition;
482 }
483
484 impl RenditionTestExt for moq_mux::catalog::Producer {
485 fn test_video(&self, name: &str) -> TestRendition {
486 TestRendition {
487 catalog: self.clone(),
488 name: name.to_string(),
489 config: None,
490 }
491 }
492
493 fn test_audio(&self, name: &str) -> TestRendition {
494 self.test_video(name)
495 }
496 }
497
498 trait CatalogTestExt {
499 fn enroll_test(&self, track: &str) -> moq_mux::Result<moq_mux::timeline::Recorder>;
500 fn media_producer<C: moq_mux::container::Container>(
501 &self,
502 track: moq_net::track::Producer,
503 container: C,
504 ) -> moq_mux::Result<moq_mux::container::Producer<C>>
505 where
506 moq_mux::Error: From<C::Error>;
507 }
508
509 impl CatalogTestExt for moq_mux::catalog::Producer {
510 fn enroll_test(&self, track: &str) -> moq_mux::Result<moq_mux::timeline::Recorder> {
511 let recorder = self.timeline().pacing_track(track)?;
512 let mut producer = self.clone();
513 let mut catalog = producer.modify()?;
514 catalog.archive.get_or_insert_with(|| self.timeline().section());
515 catalog.commit()?;
516 Ok(recorder)
517 }
518
519 fn media_producer<C: moq_mux::container::Container>(
520 &self,
521 track: moq_net::track::Producer,
522 container: C,
523 ) -> moq_mux::Result<moq_mux::container::Producer<C>>
524 where
525 moq_mux::Error: From<C::Error>,
526 {
527 let recorder = self.enroll_test(track.name())?;
528 Ok(moq_mux::container::Producer::new(track, container).with_recorder(recorder))
529 }
530 }
531
532 fn frame(micros: u64, keyframe: bool) -> moq_mux::container::Frame {
533 payload_frame(micros, keyframe, &[0xDE, 0xAD, 0xBE, 0xEF])
534 }
535
536 fn payload_frame(micros: u64, keyframe: bool, payload: &'static [u8]) -> moq_mux::container::Frame {
537 moq_mux::container::Frame {
538 timestamp: moq_net::Timestamp::from_micros(micros).unwrap(),
539 payload: bytes::Bytes::from_static(payload),
540 keyframe,
541 duration: None,
542 }
543 }
544
545 fn vp8_frame(micros: u64, keyframe: bool) -> moq_mux::container::Frame {
546 let payload = if keyframe {
547 &[0x10, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0x40, 0x01, 0xf0, 0x00][..]
549 } else {
550 &[0x31, 0x00, 0x00][..]
551 };
552 moq_mux::container::Frame {
553 timestamp: moq_net::Timestamp::from_micros(micros).unwrap(),
554 payload: bytes::Bytes::copy_from_slice(payload),
555 keyframe,
556 duration: None,
557 }
558 }
559
560 fn video_config() -> hang::catalog::VideoConfig {
561 let mut config = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8);
562 config.coded_width = Some(320);
563 config.coded_height = Some(240);
564 config.framerate = Some(30.0);
565 config
566 }
567
568 async fn settle() {
570 for _ in 0..10 {
571 tokio::task::yield_now().await;
572 }
573 }
574
575 #[tokio::test]
576 async fn escaping_broadcast_reference_is_not_advertised() {
577 let origin = produce_origin();
578 let _broadcast = origin.create_broadcast("a/pub").expect("publish allowed");
579 _broadcast.announce(Default::default()).expect("publish allowed");
580 settle().await;
581 let source = moq_mux::Source::new(origin.consume(), "a/pub");
582 let upstream = Upstream {
583 broadcast: source.broadcast().await.unwrap(),
584 source,
585 };
586 let mut config = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8);
587 config.broadcast = Some(moq_net::path::Relative::new("../../source").to_owned());
588 let mut catalog = moq_mux::catalog::hang::Catalog::default();
589 catalog.archive = Some(hang::catalog::Archive::new(hang::timeline::DEFAULT_NAME));
590 catalog.video.renditions.insert("video".to_string(), config);
591
592 let renditions = renditions::Producer::new(Config::default().window);
593 renditions.sync(&upstream, &catalog);
594 assert!(renditions.get(Kind::Video, "video").is_none());
595 }
596
597 async fn empty_upstream(origin: &moq_net::origin::Producer, path: &str) -> Upstream {
599 let source = moq_mux::Source::new(origin.consume(), path);
600 Upstream {
601 broadcast: source.broadcast().await.unwrap(),
602 source,
603 }
604 }
605
606 fn catalog_with_both() -> moq_mux::catalog::hang::Catalog {
608 let mut audio = hang::catalog::AudioConfig::new(
609 hang::catalog::AudioCodec::AAC(hang::catalog::AAC { profile: 2 }),
610 44_100,
611 2,
612 );
613 audio.description = Some(bytes::Bytes::from_static(&[0x12, 0x10]));
614 audio.bitrate = Some(96_000);
615
616 let mut catalog = moq_mux::catalog::hang::Catalog::default();
617 catalog.archive = Some(hang::catalog::Archive::new(hang::timeline::DEFAULT_NAME));
618 catalog.video.renditions.insert("video0".to_string(), video_config());
619 catalog.audio.renditions.insert("audio0".to_string(), audio);
620 catalog
621 }
622
623 async fn next_event(cursor: &mut renditions::Consumer) -> Option<renditions::Event> {
625 tokio::time::timeout(Duration::from_millis(50), cursor.next())
626 .await
627 .ok()
628 .flatten()
629 }
630
631 fn aac_avg_bitrate(init: &bytes::Bytes) -> u32 {
633 let wire = moq_mux::container::fmp4::Wire::from_init(init).unwrap();
634 let codec = &wire.trak().mdia.minf.stbl.stsd.codecs[0];
635 let moq_mux::container::fmp4::mp4_atom::Codec::Mp4a(mp4a) = codec else {
636 panic!("expected mp4a, got {codec:?}");
637 };
638 mp4a.esds.es_desc.dec_config.avg_bitrate
639 }
640
641 #[tokio::test]
646 async fn estimate_only_catalog_update_keeps_the_rendition() {
647 let origin = produce_origin();
648 let _broadcast = origin.create_broadcast("live").expect("publish allowed");
649 _broadcast.announce(Default::default()).expect("publish allowed");
650 settle().await;
651 let upstream = empty_upstream(&origin, "live").await;
652
653 let mut catalog = catalog_with_both();
654 let renditions = renditions::Producer::new(Config::default().window);
655 let mut cursor = renditions.subscribe();
656 renditions.sync(&upstream, &catalog);
657
658 let video = renditions.get(Kind::Video, "video0").expect("video rendition");
659 let audio = renditions.get(Kind::Audio, "audio0").expect("audio rendition");
660 let audio_init = audio.init().await.unwrap().expect("AAC init segment");
661 assert_eq!(
662 aac_avg_bitrate(&audio_init),
663 96_000,
664 "the muxer keeps the declared catalog bitrate"
665 );
666 assert_eq!(video.bandwidth(), 2_000_000, "the catalog carries no video bitrate yet");
667 assert_eq!(audio.bandwidth(), 96_000);
668 assert!(matches!(
669 next_event(&mut cursor).await,
670 Some(renditions::Event::Added(_))
671 ));
672 assert!(matches!(
673 next_event(&mut cursor).await,
674 Some(renditions::Event::Added(_))
675 ));
676
677 let video_config = catalog.video.renditions.get_mut("video0").unwrap();
679 video_config.bitrate = Some(1_800_000);
680 video_config.jitter = Some(Duration::from_millis(33));
681 catalog.audio.renditions.get_mut("audio0").unwrap().bitrate = Some(110_000);
682 renditions.sync(&upstream, &catalog);
683
684 assert!(
685 Arc::ptr_eq(&video, &renditions.get(Kind::Video, "video0").unwrap()),
686 "the video rendition survives an estimate-only update"
687 );
688 assert!(
689 Arc::ptr_eq(&audio, &renditions.get(Kind::Audio, "audio0").unwrap()),
690 "the audio rendition survives an estimate-only update"
691 );
692 assert_eq!(
693 video.bandwidth(),
694 1_800_000,
695 "the master playlist advertises the estimate"
696 );
697 assert_eq!(audio.bandwidth(), 110_000);
698 assert_eq!(
699 audio.init().await.unwrap().expect("cached AAC init segment"),
700 audio_init,
701 "an estimate update preserves the cached init segment"
702 );
703 assert!(next_event(&mut cursor).await.is_none(), "no rendition churn");
704
705 catalog.video.renditions.get_mut("video0").unwrap().bitrate = None;
707 renditions.sync(&upstream, &catalog);
708 assert_eq!(video.bandwidth(), 2_000_000);
709 }
710
711 #[tokio::test]
715 async fn estimated_framerate_keeps_the_rendition() {
716 let origin = produce_origin();
717 let _broadcast = origin.create_broadcast("live").expect("publish allowed");
718 _broadcast.announce(Default::default()).expect("publish allowed");
719 settle().await;
720 let upstream = empty_upstream(&origin, "live").await;
721
722 let mut catalog = catalog_with_both();
723 catalog.video.renditions.get_mut("video0").unwrap().framerate = None;
724 let renditions = renditions::Producer::new(Config::default().window);
725 let mut cursor = renditions.subscribe();
726 renditions.sync(&upstream, &catalog);
727 let video = renditions.get(Kind::Video, "video0").expect("video rendition");
728 while next_event(&mut cursor).await.is_some() {}
729
730 for framerate in [Some(30.0), Some(60.0)] {
731 catalog.video.renditions.get_mut("video0").unwrap().framerate = framerate;
732 renditions.sync(&upstream, &catalog);
733 assert!(
734 Arc::ptr_eq(&video, &renditions.get(Kind::Video, "video0").unwrap()),
735 "the video rendition survives a framerate estimate of {framerate:?}"
736 );
737 assert!(next_event(&mut cursor).await.is_none(), "no rendition churn");
738 }
739
740 let muxer = video.muxer().unwrap();
743 assert_eq!(muxer.timescale().as_u64(), 90_000);
744 let fragment = muxer.fragment(0, &[vp8_frame(0, true)]).unwrap();
745 assert_eq!(first_sample_duration(&fragment), Some(1_500), "16.7 ms at 90 kHz");
746
747 catalog.video.renditions.get_mut("video0").unwrap().framerate = Some(60_000.0 / 1001.0);
749 renditions.sync(&upstream, &catalog);
750 let fragment = video.muxer().unwrap().fragment(0, &[vp8_frame(0, true)]).unwrap();
751 assert_eq!(first_sample_duration(&fragment), Some(1_502));
752 }
753
754 fn first_sample_duration(fragment: &bytes::Bytes) -> Option<u32> {
756 use moq_mux::container::fmp4::mp4_atom::{self, DecodeMaybe};
757
758 let mut cursor = std::io::Cursor::new(fragment.as_ref());
759 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
760 if let mp4_atom::Any::Moof(moof) = atom {
761 return moof.traf[0].trun[0].entries[0].duration;
762 }
763 }
764 panic!("no moof in fragment");
765 }
766
767 #[tokio::test]
768 async fn estimate_before_first_init_reaches_aac_descriptor() {
769 let origin = produce_origin();
770 let _broadcast = origin.create_broadcast("live").expect("publish allowed");
771 _broadcast.announce(Default::default()).expect("publish allowed");
772 settle().await;
773 let upstream = empty_upstream(&origin, "live").await;
774
775 let mut catalog = catalog_with_both();
776 catalog.audio.renditions.get_mut("audio0").unwrap().bitrate = None;
777 let renditions = renditions::Producer::new(Config::default().window);
778 renditions.sync(&upstream, &catalog);
779 let audio = renditions.get(Kind::Audio, "audio0").expect("audio rendition");
780
781 catalog.audio.renditions.get_mut("audio0").unwrap().bitrate = Some(110_000);
782 renditions.sync(&upstream, &catalog);
783 assert!(
784 Arc::ptr_eq(&audio, &renditions.get(Kind::Audio, "audio0").unwrap()),
785 "the estimate updates the existing rendition"
786 );
787 let init = audio.init().await.unwrap().expect("AAC init segment");
788
789 assert_eq!(audio.bandwidth(), 110_000);
790 assert_eq!(
791 aac_avg_bitrate(&init),
792 110_000,
793 "the latest estimate reaches an init that was not cached yet"
794 );
795 }
796
797 #[tokio::test]
800 async fn decoder_config_change_rebuilds_the_rendition() {
801 let origin = produce_origin();
802 let _broadcast = origin.create_broadcast("live").expect("publish allowed");
803 _broadcast.announce(Default::default()).expect("publish allowed");
804 settle().await;
805 let upstream = empty_upstream(&origin, "live").await;
806
807 let mut catalog = catalog_with_both();
808 let renditions = renditions::Producer::new(Config::default().window);
809 let mut cursor = renditions.subscribe();
810 renditions.sync(&upstream, &catalog);
811
812 let video = renditions.get(Kind::Video, "video0").expect("video rendition");
813 let audio = renditions.get(Kind::Audio, "audio0").expect("audio rendition");
814 assert!(matches!(
815 next_event(&mut cursor).await,
816 Some(renditions::Event::Added(_))
817 ));
818 assert!(matches!(
819 next_event(&mut cursor).await,
820 Some(renditions::Event::Added(_))
821 ));
822
823 catalog.video.renditions.get_mut("video0").unwrap().coded_width = Some(640);
825 catalog.audio.renditions.get_mut("audio0").unwrap().sample_rate = 48_000;
826 renditions.sync(&upstream, &catalog);
827
828 let rebuilt = renditions.get(Kind::Video, "video0").expect("video rendition");
829 assert!(!Arc::ptr_eq(&video, &rebuilt), "the video rendition is rebuilt");
830 assert_eq!(rebuilt.width, Some(640));
831 assert!(
832 !Arc::ptr_eq(&audio, &renditions.get(Kind::Audio, "audio0").unwrap()),
833 "the audio rendition is rebuilt"
834 );
835
836 let mut removed = 0;
838 let mut added = 0;
839 while let Some(event) = next_event(&mut cursor).await {
840 match event {
841 renditions::Event::Removed { .. } => removed += 1,
842 renditions::Event::Added(_) => added += 1,
843 }
844 }
845 assert_eq!((removed, added), (2, 2));
846 }
847
848 #[tokio::test]
852 async fn master_lists_only_playable_renditions() {
853 let origin = produce_origin();
854 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
855 broadcast.announce(Default::default()).expect("publish allowed");
856 settle().await;
857 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
858
859 let reserved = catalog.reserve();
860 let mut registration = catalog.test_video("video0").unwrap();
861 registration.set(video_config()).unwrap();
862 drop(reserved);
863
864 let track = broadcast.create_track("video0", None).unwrap();
865 let mut media = catalog
866 .media_producer(
867 track,
868 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
869 )
870 .unwrap();
871
872 let source = moq_mux::Source::new(origin.consume(), "live");
873 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
874 let rendition = tokio::time::timeout(Duration::from_secs(5), async {
875 loop {
876 if let Some(rendition) = broadcaster.rendition(Kind::Video, "video0") {
877 break rendition;
878 }
879 tokio::task::yield_now().await;
880 }
881 })
882 .await
883 .expect("rendition discovered from the catalog");
884
885 assert!(
886 rendition.media_playlist(None).is_none(),
887 "rendition is not playable yet"
888 );
889 assert!(
890 !broadcaster.master_playlist(None).contains("video/video0/media.m3u8"),
891 "master advertised a media playlist that would return 404"
892 );
893 assert!(
894 tokio::time::timeout(Duration::from_millis(10), broadcaster.ready())
895 .await
896 .is_err(),
897 "broadcaster became ready before it had a playable rendition"
898 );
899
900 media.write(vp8_frame(0, true)).unwrap();
901 media.write(vp8_frame(2_000_000, true)).unwrap();
902 media.write(vp8_frame(4_000_000, true)).unwrap();
903 tokio::time::timeout(Duration::from_secs(5), broadcaster.ready())
904 .await
905 .expect("broadcaster becomes ready with a complete segment");
906 assert!(broadcaster.master_playlist(None).contains("video/video0/media.m3u8"));
907
908 drop((media, registration, broadcast));
909 }
910
911 fn between<'a>(haystack: &'a str, start: &str, end: &str) -> &'a str {
913 let rest = &haystack[haystack.find(start).unwrap_or_else(|| panic!("no {start:?}")) + start.len()..];
914 &rest[..rest.find(end).unwrap_or_else(|| panic!("no {end:?}"))]
915 }
916
917 fn init_hash(playlist: &str) -> &str {
919 between(playlist, "#EXT-X-MAP:URI=\"init.", ".mp4")
920 }
921
922 fn publish_vp8(
924 broadcast: &mut moq_net::broadcast::Producer,
925 ) -> (
926 moq_mux::catalog::Producer,
927 TestRendition,
928 moq_mux::container::Producer<moq_mux::catalog::hang::Container>,
929 ) {
930 let catalog = moq_mux::catalog::Producer::new(broadcast, moq_mux::catalog::Config::default()).unwrap();
931 let reserved = catalog.reserve();
932 let mut registration = catalog.test_video("video0").unwrap();
933 registration.set(video_config()).unwrap();
934 drop(reserved);
935 let track = broadcast.create_track("video0", None).unwrap();
936 let mut media = catalog
937 .media_producer(
938 track,
939 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
940 )
941 .unwrap();
942 for (micros, keyframe) in [(0, true), (1_000_000, false), (2_000_000, true), (4_000_000, true)] {
943 media.write(vp8_frame(micros, keyframe)).unwrap();
944 }
945 (catalog, registration, media)
946 }
947
948 #[tokio::test(start_paused = true)]
951 async fn a_transmuxed_segment_keeps_its_init_with_an_unfinished_tail() {
952 let origin = produce_origin();
953 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
954 let (mut catalog, mut registration, _media) = publish_vp8(&mut broadcast);
955 registration
956 .set(hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8))
957 .unwrap();
958 broadcast.announce(moq_net::origin::Route::default()).unwrap();
959 settle().await;
960 let source = moq_mux::Source::new(origin.consume(), "live");
961 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
962 broadcaster.ready().await;
963 let rendition = broadcaster.rendition(Kind::Video, "video0").expect("rendition");
964 let mut cursor = rendition.segments();
965 let segment = cursor.next().await.unwrap().expect("completed segment");
966 assert!(!segment.media.is_empty());
967
968 catalog.finish().unwrap();
969 while !rendition.snapshot().finished {
970 tokio::task::yield_now().await;
971 }
972 let init = tokio::time::timeout(Duration::from_secs(1), rendition.init())
973 .await
974 .expect("init must not wait for the unfinished trailing group")
975 .unwrap()
976 .expect("transmuxed segment has an init");
977 assert_eq!(&init[4..8], b"ftyp");
978 }
979
980 #[tokio::test(start_paused = true)]
983 async fn a_blocked_init_bootstrap_does_not_hide_a_cached_init() {
984 let origin = produce_origin();
985 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
986 let (mut catalog, mut registration, _media) = publish_vp8(&mut broadcast);
987 registration
988 .set(hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8))
989 .unwrap();
990 broadcast.announce(moq_net::origin::Route::default()).unwrap();
991 settle().await;
992 let source = moq_mux::Source::new(origin.consume(), "live");
993 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
994 broadcaster.ready().await;
995 let rendition = broadcaster.rendition(Kind::Video, "video0").expect("rendition");
996
997 catalog.finish().unwrap();
998 while !rendition.snapshot().finished {
999 tokio::task::yield_now().await;
1000 }
1001 let mut blocked = std::pin::pin!(rendition.init());
1002 assert!(
1003 tokio::time::timeout(Duration::from_millis(10), blocked.as_mut())
1004 .await
1005 .is_err(),
1006 "the bootstrap reads the unfinished trailing group"
1007 );
1008
1009 let segment = rendition.segments().next().await.unwrap().expect("completed segment");
1010 assert!(!segment.media.is_empty());
1011 tokio::time::timeout(Duration::from_secs(1), rendition.init())
1012 .await
1013 .expect("init must not wait behind the blocked bootstrap")
1014 .unwrap()
1015 .expect("transmuxed segment has an init");
1016 }
1017
1018 #[tokio::test]
1021 async fn a_reconfigure_versions_the_init_url() {
1022 let origin = produce_origin();
1023 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1024 broadcast.announce(Default::default()).expect("publish allowed");
1025 settle().await;
1026 let (_catalog, mut registration, _media) = publish_vp8(&mut broadcast);
1027
1028 let source = moq_mux::Source::new(origin.consume(), "live");
1029 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1030 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1031 let old = broadcaster.rendition(Kind::Video, "video0").expect("rendition");
1032 let old_playlist = old
1033 .media_playlist(None)
1034 .expect("an out-of-band init renders without a fetch");
1035 let old_hash = init_hash(&old_playlist).to_string();
1036
1037 let mut config = video_config();
1038 config.coded_width = Some(640);
1039 config.coded_height = Some(480);
1040 registration.set(config).unwrap();
1041
1042 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
1043 let new = loop {
1044 let current = broadcaster.rendition(Kind::Video, "video0").expect("rendition");
1045 if !Arc::ptr_eq(¤t, &old) {
1046 break current;
1047 }
1048 assert!(
1049 tokio::time::Instant::now() < deadline,
1050 "the reconfigure never rebuilt the rendition"
1051 );
1052 tokio::time::sleep(Duration::from_millis(10)).await;
1053 };
1054 let new_init = new.init().await.unwrap().expect("init segment");
1055 let _ = tokio::time::timeout(Duration::from_secs(5), new.playable()).await;
1056 let new_playlist = new.media_playlist(None).expect("playable");
1057 let new_hash = init_hash(&new_playlist);
1058 assert_ne!(new_hash, old_hash, "different init bytes get a different URL");
1059 assert_eq!(new.init_versioned(new_hash).await.unwrap(), Some(new_init));
1060 assert!(new.init_versioned(&old_hash).await.unwrap().is_none());
1061 }
1062
1063 #[tokio::test]
1066 async fn a_generation_versions_segment_urls_and_a_new_one_resets_the_window() {
1067 let origin = produce_origin();
1068 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1069 broadcast.announce(Default::default()).expect("publish allowed");
1070 settle().await;
1071 let (_catalog, _registration, mut media) = publish_vp8(&mut broadcast);
1072
1073 let source = moq_mux::Source::new(origin.consume(), "live");
1074 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1075 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1076 let rendition = broadcaster.rendition(Kind::Video, "video0").expect("rendition");
1077 let unversioned = rendition.media_playlist(None).expect("playable");
1078 assert!(unversioned.contains("\nseg/0.m4s\n"));
1079
1080 assert!(matches!(
1081 broadcaster.set_generation(Some("run.1")),
1082 Err(crate::Error::InvalidGeneration(_))
1083 ));
1084 assert!(broadcaster.set_generation(Some("")).is_err());
1085
1086 broadcaster.set_generation(Some("run-1")).unwrap();
1087 let labeled = rendition
1088 .media_playlist(None)
1089 .expect("the first generation keeps the rows");
1090 assert!(labeled.contains("\nseg/run-1.0.m4s\n"));
1091 assert!(labeled.contains("\nseg/run-1.1.m4s\n"));
1092 assert_eq!(
1093 init_hash(&labeled),
1094 init_hash(&unversioned),
1095 "the init needs no generation"
1096 );
1097 let manifest = broadcaster.manifest(None).expect("manifest");
1098 assert!(manifest.contains("media=\"video/video0/seg/run-1.t$Time$.m4s\""));
1099
1100 broadcaster.set_generation(Some("run-2")).unwrap();
1101 assert!(
1102 rendition.media_playlist(None).is_none(),
1103 "a new generation drops the old run's rows"
1104 );
1105
1106 media.write(vp8_frame(6_000_000, true)).unwrap();
1107 let _ = tokio::time::timeout(Duration::from_secs(5), rendition.playable()).await;
1108 let restarted = rendition.media_playlist(None).expect("the new run lists");
1109 assert!(restarted.contains("\nseg/run-2.2.m4s\n"));
1110 assert!(!restarted.contains("run-1"));
1111
1112 broadcaster.set_generation(None).unwrap();
1113 assert!(
1114 rendition.media_playlist(None).is_none(),
1115 "clearing the generation is a change too"
1116 );
1117 }
1118
1119 #[tokio::test]
1122 async fn a_new_generation_rebuilds_an_inline_init() {
1123 let origin = produce_origin();
1124 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1125 broadcast.announce(Default::default()).expect("publish allowed");
1126 settle().await;
1127 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1128 let reserved = catalog.reserve();
1129 let mut registration = catalog.test_video("video0").unwrap();
1130 let mut config = video_config();
1131 config.coded_width = None;
1132 config.coded_height = None;
1133 registration.set(config).unwrap();
1134 drop(reserved);
1135 let track = broadcast.create_track("video0", None).unwrap();
1136 let mut media = catalog
1137 .media_producer(
1138 track,
1139 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1140 )
1141 .unwrap();
1142 for micros in [0, 2_000_000, 4_000_000] {
1143 media.write(vp8_frame(micros, true)).unwrap();
1144 }
1145
1146 let source = moq_mux::Source::new(origin.consume(), "live");
1147 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1148 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1149 let rendition = broadcaster.rendition(Kind::Video, "video0").expect("rendition");
1150 let _ = tokio::time::timeout(Duration::from_secs(5), rendition.playable()).await;
1151 let old = rendition.init().await.unwrap().expect("init segment");
1152 broadcaster.set_generation(Some("run-1")).unwrap();
1153 broadcaster.set_generation(Some("run-2")).unwrap();
1154
1155 for micros in [6_000_000, 8_000_000, 10_000_000] {
1157 media
1158 .write(moq_mux::container::Frame {
1159 timestamp: moq_net::Timestamp::from_micros(micros).unwrap(),
1160 payload: bytes::Bytes::from_static(&[0x10, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0x80, 0x02, 0xe0, 0x01]),
1161 keyframe: true,
1162 duration: None,
1163 })
1164 .unwrap();
1165 }
1166 let _ = tokio::time::timeout(Duration::from_secs(5), rendition.playable()).await;
1167 let new = rendition.init().await.unwrap().expect("init segment");
1168 assert_ne!(new, old, "the new run's init is built from its own media");
1169 let playlist = rendition.media_playlist(None).expect("playable");
1170 assert_eq!(rendition.init_versioned(init_hash(&playlist)).await.unwrap(), Some(new));
1171 }
1172
1173 #[tokio::test]
1174 async fn serves_playlist_and_segments_from_the_timeline() {
1175 let origin = produce_origin();
1176 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1177 broadcast.announce(Default::default()).expect("publish allowed");
1178 settle().await;
1179 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1180
1181 let reserved = catalog.reserve();
1182 let mut registration = catalog.test_video("video0").unwrap();
1183 let mut config = video_config();
1184 config.coded_width = None;
1185 config.coded_height = None;
1186 registration.set(config).unwrap();
1187 drop(reserved);
1188
1189 let track = broadcast.create_track("video0", None).unwrap();
1191 let mut media = catalog
1192 .media_producer(
1193 track,
1194 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1195 )
1196 .unwrap();
1197 media.write(vp8_frame(0, true)).unwrap();
1198 media.write(vp8_frame(1_000_000, false)).unwrap();
1199 media.write(vp8_frame(2_000_000, true)).unwrap();
1200 media.write(vp8_frame(3_000_000, false)).unwrap();
1201 media.write(vp8_frame(4_000_000, true)).unwrap();
1202
1203 let source = moq_mux::Source::new(origin.consume(), "live");
1204 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1205 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1206 let rendition = broadcaster
1207 .rendition(Kind::Video, "video0")
1208 .expect("rendition discovered from the catalog");
1209 let _ = tokio::time::timeout(Duration::from_secs(5), rendition.playable()).await;
1210
1211 let master = broadcaster.master_playlist(None);
1212 assert!(master.contains("video/video0/media.m3u8"), "master lists the rendition");
1213
1214 let playlist = rendition.snapshot();
1215 assert_eq!(playlist.segments.len(), 2, "the live-edge group is not listed");
1216 assert_eq!(playlist.segments[0].segment, 0);
1217 assert_eq!(playlist.segments[0].duration, Duration::from_secs(2));
1218 assert_eq!(playlist.segments[1].segment, 1);
1219 assert_eq!(
1220 playlist.target_duration, 2,
1221 "the observed segment duration, in whole seconds"
1222 );
1223 assert!(!playlist.finished);
1224
1225 assert!(rendition.media_playlist(None).is_none());
1228 let init = rendition.init().await.unwrap().expect("init segment");
1229 assert_eq!(&init[4..8], b"ftyp");
1230
1231 let rendered = rendition.media_playlist(None).expect("playable");
1232 let hash = init_hash(&rendered);
1233 assert!(rendered.contains("seg/0.m4s\n"));
1234 assert!(rendered.contains("seg/1.m4s\n"));
1235 assert_eq!(rendition.init_versioned(hash).await.unwrap(), Some(init));
1236 assert!(rendition.init_versioned("0000000000000000").await.unwrap().is_none());
1237
1238 let signed = rendition.media_playlist(Some("jwt=abc.def")).expect("playable");
1240 assert!(signed.contains(&format!("#EXT-X-MAP:URI=\"init.{hash}.mp4?jwt=abc.def\"\n")));
1241 assert!(signed.contains("seg/0.m4s?jwt=abc.def\n"));
1242
1243 let segment = rendition.segment(0).await.unwrap().expect("segment fetched on demand");
1244 assert_eq!(&segment[4..8], b"moof", "a fetched group transmuxes to moof+mdat");
1245
1246 assert!(rendition.segment(2).await.unwrap().is_none());
1248 assert!(rendition.segment(99).await.unwrap().is_none());
1249
1250 drop((media, registration, broadcast));
1252 }
1253
1254 #[tokio::test]
1256 async fn a_duration_marker_times_the_hls_trailing_sample() {
1257 let origin = produce_origin();
1258 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1259 broadcast.announce(Default::default()).expect("publish allowed");
1260 settle().await;
1261 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1262
1263 let reserved = catalog.reserve();
1264 let mut registration = catalog.test_video("video0").unwrap();
1265 registration.set(video_config()).unwrap();
1266 drop(reserved);
1267
1268 let track = broadcast.create_track("video0", None).unwrap();
1269 let mut media = catalog
1270 .media_producer(
1271 track,
1272 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Video),
1273 )
1274 .unwrap();
1275 media.write(vp8_frame(0, true)).unwrap();
1276 media.write(vp8_frame(1_000_000, false)).unwrap();
1277 media.write(vp8_frame(2_000_000, true)).unwrap();
1278 media.finish().unwrap();
1279
1280 let source = moq_mux::Source::new(origin.consume(), "live");
1281 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1282 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1283 let rendition = broadcaster
1284 .rendition(Kind::Video, "video0")
1285 .expect("rendition discovered from the catalog");
1286 let _ = tokio::time::timeout(Duration::from_secs(5), rendition.playable()).await;
1287
1288 let segment = rendition.segment(0).await.unwrap().expect("segment fetched on demand");
1289 assert_eq!(&segment[4..8], b"moof");
1290 drop((media, registration, broadcast));
1291 }
1292
1293 #[tokio::test]
1297 async fn serves_dash_manifest_and_time_addressed_segments() {
1298 let origin = produce_origin();
1299 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1300 broadcast.announce(Default::default()).expect("publish allowed");
1301 settle().await;
1302 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1303
1304 let reserved = catalog.reserve();
1305 let mut video_registration = catalog.test_video("video0").unwrap();
1306 video_registration.set(video_config()).unwrap();
1307 let mut audio_registration = catalog.test_audio("audio0").unwrap();
1308 let audio_config = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2);
1309 audio_registration.set(audio_config).unwrap();
1310 drop(reserved);
1311
1312 let video_track = broadcast.create_track("video0", None).unwrap();
1313 let mut video = catalog
1314 .media_producer(
1315 video_track,
1316 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1317 )
1318 .unwrap();
1319 let audio_track = broadcast.create_track("audio0", None).unwrap();
1320 let mut audio = catalog
1321 .media_producer(
1322 audio_track,
1323 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1324 )
1325 .unwrap();
1326
1327 let mut audio_write = |micros: u64| {
1329 let keyframe = audio.needs_keyframe();
1330 audio.write(frame(micros, keyframe)).unwrap();
1331 audio.cut(None).unwrap();
1332 };
1333 video.write(frame(0, true)).unwrap();
1334 audio_write(0);
1335 video.write(frame(2_000_000, true)).unwrap();
1336 audio_write(2_000_000);
1337 video.write(frame(4_000_000, true)).unwrap(); audio_write(4_000_000);
1339
1340 let source = moq_mux::Source::new(origin.consume(), "live");
1341 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1342 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1343 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.playable()).await;
1344 let video_rendition = broadcaster.rendition(Kind::Video, "video0").expect("video discovered");
1345 let audio_rendition = broadcaster.rendition(Kind::Audio, "audio0").expect("audio discovered");
1346 let _ = tokio::time::timeout(Duration::from_secs(5), video_rendition.playable()).await;
1347 let _ = tokio::time::timeout(Duration::from_secs(5), audio_rendition.playable()).await;
1348
1349 let manifest = broadcaster.manifest(None).expect("manifest renders once playable");
1350 assert!(manifest.contains(" type=\"dynamic\""), "live broadcast is dynamic");
1351 assert!(
1352 manifest.contains(" availabilityStartTime=\""),
1353 "pts 0 is anchored by the catalog root clock"
1354 );
1355 assert!(manifest.contains("id=\"video/video0\""));
1356 assert!(manifest.contains("id=\"audio/audio0\""));
1357 assert!(manifest.contains("media=\"video/video0/seg/t$Time$.m4s\""));
1358 assert_eq!(manifest.matches("<S t=\"0\" d=\"2000\"/>").count(), 2);
1360 assert_eq!(manifest.matches("<S t=\"2000\" d=\"2000\"/>").count(), 2);
1361 assert!(!manifest.contains("<S t=\"4000\""), "the live-edge group is not listed");
1362
1363 let signed = broadcaster.manifest(Some("jwt=abc.def")).expect("manifest renders");
1365 let hash = between(&signed, "initialization=\"video/video0/init.", ".mp4?jwt=abc.def\"");
1366 let init = video_rendition.init().await.unwrap().expect("init segment");
1367 assert_eq!(video_rendition.init_versioned(hash).await.unwrap(), Some(init));
1368 assert!(signed.contains("media=\"video/video0/seg/t$Time$.m4s?jwt=abc.def\""));
1369
1370 let by_time = video_rendition
1372 .segment_at(2_000)
1373 .await
1374 .unwrap()
1375 .expect("segment fetched by pts");
1376 let by_number = video_rendition.segment(1).await.unwrap().expect("segment by number");
1377 assert_eq!(by_time, by_number);
1378 assert!(video_rendition.segment_at(999).await.unwrap().is_none());
1379
1380 drop((video, audio, video_registration, audio_registration, broadcast));
1381 }
1382
1383 #[tokio::test]
1387 async fn root_clock_times_playlists_and_manifest() {
1388 use std::time::{SystemTime, UNIX_EPOCH};
1389
1390 let origin = produce_origin();
1391 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1392 broadcast.announce(Default::default()).expect("publish allowed");
1393 settle().await;
1394
1395 let wall = UNIX_EPOCH + Duration::from_millis(hang::catalog::MOQ_EPOCH_UNIX_MILLIS);
1397 let config = moq_mux::catalog::Config::default()
1398 .with_clock(moq_mux::Clock::at(std::time::Instant::now(), wall).expect("a representable wall"));
1399 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config).unwrap();
1400
1401 let reserved = catalog.reserve();
1402 let mut registration = catalog.test_video("video0").unwrap();
1403 registration.set(video_config()).unwrap();
1404 drop(reserved);
1405
1406 let track = broadcast.create_track("video0", None).unwrap();
1407 let mut media = catalog
1408 .media_producer(
1409 track,
1410 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1411 )
1412 .unwrap();
1413 media.write(frame(0, true)).unwrap();
1414 media.write(frame(2_000_000, true)).unwrap();
1415 media.write(frame(4_000_000, true)).unwrap(); let source = moq_mux::Source::new(origin.consume(), "live");
1418 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1419 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1420 let rendition = broadcaster.rendition(Kind::Video, "video0").expect("video discovered");
1421 let _ = tokio::time::timeout(Duration::from_secs(5), rendition.playable()).await;
1422
1423 let snapshot = rendition.snapshot();
1425 assert_eq!(
1426 snapshot.program_date_time,
1427 Some(SystemTime::UNIX_EPOCH + Duration::from_millis(hang::catalog::MOQ_EPOCH_UNIX_MILLIS))
1428 );
1429 let playlist = rendition.media_playlist(None).expect("playlist renders");
1430 assert!(
1431 playlist.contains("#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.000Z\n"),
1432 "pts 0 maps through the root clock: {playlist}"
1433 );
1434
1435 let manifest = broadcaster.manifest(None).expect("manifest renders once playable");
1436 assert!(
1437 manifest.contains(" availabilityStartTime=\"2020-01-01T00:00:00.000Z\""),
1438 "the manifest anchors at the root clock: {manifest}"
1439 );
1440
1441 drop((media, registration, broadcast));
1442 }
1443
1444 #[tokio::test]
1447 async fn manifest_falls_back_to_arrival_anchor_without_a_clock() {
1448 let origin = produce_origin();
1449 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1450 broadcast.announce(Default::default()).expect("publish allowed");
1451 settle().await;
1452 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1453
1454 let reserved = catalog.reserve();
1455 catalog.modify().unwrap().clock = None;
1458 let mut registration = catalog.test_video("video0").unwrap();
1459 registration.set(video_config()).unwrap();
1460 drop(reserved);
1461 assert_eq!(catalog.snapshot().clock, None);
1462
1463 let track = broadcast.create_track("video0", None).unwrap();
1464 let mut media = catalog
1465 .media_producer(
1466 track,
1467 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1468 )
1469 .unwrap();
1470 media.write(frame(0, true)).unwrap();
1471 media.write(frame(2_000_000, true)).unwrap();
1472 media.write(frame(4_000_000, true)).unwrap(); let source = moq_mux::Source::new(origin.consume(), "live");
1475 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1476 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1477 let rendition = broadcaster.rendition(Kind::Video, "video0").expect("video discovered");
1478 let _ = tokio::time::timeout(Duration::from_secs(5), rendition.playable()).await;
1479
1480 assert_eq!(rendition.snapshot().program_date_time, None);
1481 let playlist = rendition.media_playlist(None).expect("playlist renders");
1482 assert!(!playlist.contains("PROGRAM-DATE-TIME"), "{playlist}");
1483
1484 let manifest = broadcaster.manifest(None).expect("manifest renders once playable");
1485 assert!(
1486 manifest.contains(" availabilityStartTime=\""),
1487 "pts 0 is anchored even without a catalog clock"
1488 );
1489
1490 drop((media, registration, broadcast));
1491 }
1492
1493 #[tokio::test]
1495 async fn dash_manifest_turns_static_when_finished() {
1496 let origin = produce_origin();
1497 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1498 broadcast.announce(Default::default()).expect("publish allowed");
1499 settle().await;
1500 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1501
1502 let reserved = catalog.reserve();
1503 let mut registration = catalog.test_video("video0").unwrap();
1504 registration.set(video_config()).unwrap();
1505 drop(reserved);
1506
1507 let track = broadcast.create_track("video0", None).unwrap();
1508 let mut media = catalog
1509 .media_producer(
1510 track,
1511 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1512 )
1513 .unwrap();
1514 media.write(frame(0, true)).unwrap();
1515 media.write(frame(2_000_000, true)).unwrap();
1516
1517 let source = moq_mux::Source::new(origin.consume(), "live");
1518 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1519 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1520 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.playable()).await;
1521
1522 media.finish().unwrap();
1524 catalog.finish().unwrap();
1525
1526 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
1527 let manifest = loop {
1528 let manifest = broadcaster.manifest(None).expect("manifest renders");
1529 if manifest.contains(" type=\"static\"") {
1530 break manifest;
1531 }
1532 assert!(tokio::time::Instant::now() < deadline, "manifest never turned static");
1533 tokio::time::sleep(Duration::from_millis(50)).await;
1534 };
1535 assert!(manifest.contains("<S t=\"0\" d=\"2000\"/>"));
1540 assert!(
1541 !manifest.contains("<S t=\"2000\""),
1542 "the zero-duration tail is not listed"
1543 );
1544 assert!(manifest.contains(" mediaPresentationDuration=\"PT2.000S\""));
1545 assert!(!manifest.contains("presentationTimeOffset"));
1546 assert!(!manifest.contains("availabilityStartTime"));
1547
1548 drop((catalog, media, registration, broadcast));
1549 }
1550
1551 #[tokio::test]
1558 async fn target_duration_covers_the_window_without_a_declared_bound() {
1559 let origin = produce_origin();
1560 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1561 broadcast.announce(Default::default()).expect("publish allowed");
1562 settle().await;
1563 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1564
1565 let reserved = catalog.reserve();
1566 let mut registration = catalog.test_video("video0").unwrap();
1567 registration
1568 .set(hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8))
1569 .unwrap();
1570 drop(reserved);
1571
1572 let track = broadcast.create_track("video0", None).unwrap();
1574 let mut media = catalog
1575 .media_producer(
1576 track,
1577 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1578 )
1579 .unwrap();
1580 media.write(frame(0, true)).unwrap();
1581 media.write(frame(3_000_000, true)).unwrap();
1582 media.write(frame(6_000_000, true)).unwrap();
1583
1584 let source = moq_mux::Source::new(origin.consume(), "live");
1585 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1586 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1587 let rendition = broadcaster.rendition(Kind::Video, "video0").expect("rendition");
1588 let _ = tokio::time::timeout(Duration::from_secs(5), rendition.playable()).await;
1589
1590 let playlist = rendition.snapshot();
1591 assert_eq!(playlist.segments[0].duration, Duration::from_secs(3));
1592 assert_eq!(
1593 playlist.target_duration, 3,
1594 "no bound was declared, so the target duration must still cover the 3s segment"
1595 );
1596 }
1597
1598 #[tokio::test]
1599 async fn audio_and_video_segments_are_aligned() {
1600 let origin = produce_origin();
1601 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1602 broadcast.announce(Default::default()).expect("publish allowed");
1603 settle().await;
1604 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1605
1606 let reserved = catalog.reserve();
1607 let mut video_registration = catalog.test_video("video0").unwrap();
1608 video_registration.set(video_config()).unwrap();
1609 let mut audio_registration = catalog.test_audio("audio0").unwrap();
1610 let audio_config = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2);
1611 audio_registration.set(audio_config).unwrap();
1612 drop(reserved);
1613
1614 let video_track = broadcast.create_track("video0", None).unwrap();
1615 let mut video = catalog
1616 .media_producer(
1617 video_track,
1618 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1619 )
1620 .unwrap();
1621 let audio_track = broadcast.create_track("audio0", None).unwrap();
1622 let mut audio = catalog
1623 .media_producer(
1624 audio_track,
1625 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1626 )
1627 .unwrap();
1628
1629 let mut audio_write = |micros: u64| {
1632 let keyframe = audio.needs_keyframe();
1633 audio.write(frame(micros, keyframe)).unwrap();
1634 audio.cut(None).unwrap();
1635 };
1636 video.write(frame(0, true)).unwrap(); for micros in [0u64, 500_000, 1_000_000, 1_500_000] {
1638 audio_write(micros); }
1640 video.write(frame(2_000_000, true)).unwrap(); for micros in [2_000_000u64, 2_500_000, 3_000_000, 3_500_000] {
1642 audio_write(micros); }
1644 video.write(frame(4_000_000, true)).unwrap(); audio_write(4_000_000); let source = moq_mux::Source::new(origin.consume(), "live");
1648 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1649 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1650 let video_rendition = broadcaster.rendition(Kind::Video, "video0").expect("video discovered");
1651 let audio_rendition = broadcaster.rendition(Kind::Audio, "audio0").expect("audio discovered");
1652 let _ = tokio::time::timeout(Duration::from_secs(5), video_rendition.playable()).await;
1653 let _ = tokio::time::timeout(Duration::from_secs(5), audio_rendition.playable()).await;
1654
1655 let video_playlist = video_rendition.snapshot();
1657 let audio_playlist = audio_rendition.snapshot();
1658 assert_eq!(video_playlist.media_sequence, audio_playlist.media_sequence);
1659 let video_segments: Vec<u64> = video_playlist.segments.iter().map(|s| s.segment).collect();
1660 let audio_segments: Vec<u64> = audio_playlist.segments.iter().map(|s| s.segment).collect();
1661 assert_eq!(video_segments, vec![0, 1]);
1662 assert_eq!(audio_segments, vec![0, 1], "audio lists the same aligned segments");
1663 assert_eq!(
1664 audio_playlist.segments[0].duration,
1665 Duration::from_secs(2),
1666 "cut at the video boundary"
1667 );
1668
1669 let rendered = audio_rendition.media_playlist(None).expect("playable");
1671 assert!(rendered.contains("seg/0.m4s\n"));
1672 assert!(rendered.contains("seg/1.m4s\n"));
1673
1674 let video_segment = video_rendition.segment(1).await.unwrap().expect("video segment");
1676 assert_eq!(&video_segment[4..8], b"moof");
1677 let audio_segment = audio_rendition.segment(1).await.unwrap().expect("audio segment");
1678 assert_eq!(&audio_segment[4..8], b"moof");
1679 assert!(
1680 audio_segment.len() > video_segment.len(),
1681 "the audio segment packs multiple groups into one fragment"
1682 );
1683
1684 drop((video, audio, video_registration, audio_registration, broadcast));
1685 }
1686
1687 #[tokio::test]
1693 async fn dropping_the_broadcaster_keeps_a_cursor_drainable() {
1694 let origin = produce_origin();
1695 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1696 broadcast.announce(Default::default()).expect("publish allowed");
1697 settle().await;
1698 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1699
1700 let reserved = catalog.reserve();
1701 let mut registration = catalog.test_video("video0").unwrap();
1702 let config = video_config();
1703 registration.set(config).unwrap();
1704 drop(reserved);
1705
1706 let track = broadcast.create_track("video0", None).unwrap();
1709 let mut media = catalog
1710 .media_producer(
1711 track,
1712 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1713 )
1714 .unwrap();
1715 media.write(frame(0, true)).unwrap();
1716 media.write(frame(2_000_000, true)).unwrap();
1717
1718 let source = moq_mux::Source::new(origin.consume(), "live");
1719 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
1720 let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await;
1721 let rendition = broadcaster
1722 .rendition(Kind::Video, "video0")
1723 .expect("rendition discovered");
1724 let mut segments = rendition.segments();
1725
1726 drop(broadcaster);
1728
1729 media.finish().unwrap();
1732 catalog.finish().unwrap();
1733
1734 let mut groups = Vec::new();
1735 while let Some(segment) = tokio::time::timeout(Duration::from_secs(5), segments.next())
1736 .await
1737 .expect("the cursor drains without parking")
1738 .unwrap()
1739 {
1740 groups.push(segment.segment);
1741 }
1742 assert!(
1743 groups.contains(&1),
1744 "the final segment survives dropping the broadcaster, got {groups:?}"
1745 );
1746
1747 drop((catalog, media, registration, broadcast, rendition));
1748 }
1749
1750 fn contains(haystack: &bytes::Bytes, needle: &[u8]) -> bool {
1753 haystack.windows(needle.len()).any(|window| window == needle)
1754 }
1755
1756 fn export(
1759 upstream: &Upstream,
1760 config: &hang::catalog::VideoConfig,
1761 ) -> (Arc<Rendition>, tokio::task::JoinHandle<()>) {
1762 let mut catalog = moq_mux::catalog::hang::Catalog::default();
1763 catalog.video.renditions.insert("video0".to_string(), config.clone());
1764 let archive = hang::catalog::Archive::new(hang::timeline::DEFAULT_NAME);
1765 catalog.archive = Some(archive.clone());
1766
1767 let renditions = renditions::Producer::new(Config::default().window);
1768 renditions.sync(upstream, &catalog);
1769 let watcher = tokio::spawn(watch_timeline(upstream.broadcast.clone(), archive, renditions.fanout()));
1770 let rendition = renditions.get(Kind::Video, "video0").expect("rendition synced");
1771 (rendition, watcher)
1772 }
1773
1774 #[tokio::test]
1780 async fn a_replacement_publisher_is_not_served_under_the_replaced_catalog() {
1781 const OLD: &[u8] = b"OLDOLDOLDOLDOLDO";
1782 const NEW: &[u8] = b"NEWNEWNEWNEWNEWN";
1783
1784 fn publish(
1788 origin: &moq_net::origin::Producer,
1789 payload: &'static [u8],
1790 ) -> (Box<dyn std::any::Any>, hang::catalog::VideoConfig) {
1791 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
1792 broadcast.announce(Default::default()).expect("publish allowed");
1793 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1794
1795 let reserved = catalog.reserve();
1796 let mut registration = catalog.test_video("video0").unwrap();
1797 let config = video_config();
1798 registration.set(config.clone()).unwrap();
1799 drop(reserved);
1800
1801 let track = broadcast.create_track("video0", None).unwrap();
1802 let mut media = catalog
1803 .media_producer(
1804 track,
1805 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1806 )
1807 .unwrap();
1808 media.write(payload_frame(0, true, payload)).unwrap();
1809 media.write(payload_frame(2_000_000, true, payload)).unwrap();
1810 media.write(payload_frame(4_000_000, true, payload)).unwrap();
1811
1812 (Box::new((broadcast, catalog, registration, media)), config)
1813 }
1814
1815 let origin = produce_origin();
1816 let (old, config) = publish(&origin, OLD);
1817 settle().await;
1818
1819 let source = moq_mux::Source::new(origin.consume(), "live");
1821 let upstream = Upstream {
1822 broadcast: source.broadcast().await.unwrap(),
1823 source,
1824 };
1825
1826 drop(old);
1830 let (new, _) = publish(&origin, NEW);
1831 settle().await;
1832
1833 let (rendition, watcher) = export(&upstream, &config);
1834 let _ = tokio::time::timeout(Duration::from_secs(1), rendition.playable()).await;
1835 let served = rendition.segment(0).await.unwrap();
1836 if let Some(served) = &served {
1837 assert!(
1838 !contains(served, NEW),
1839 "the replacement publisher's media was served under the replaced broadcast's catalog"
1840 );
1841 }
1842 assert!(
1843 served.is_none(),
1844 "the replaced broadcast is closed, so it serves nothing"
1845 );
1846 watcher.abort();
1847
1848 let source = moq_mux::Source::new(origin.consume(), "live");
1852 let fresh = Upstream {
1853 broadcast: source.broadcast().await.unwrap(),
1854 source,
1855 };
1856 let (rendition, watcher) = export(&fresh, &config);
1857 tokio::time::timeout(Duration::from_secs(5), rendition.playable())
1858 .await
1859 .expect("the new publisher's timeline arrives");
1860 let served = rendition
1861 .segment(0)
1862 .await
1863 .unwrap()
1864 .expect("the new publisher is servable on its own catalog");
1865 assert!(
1866 contains(&served, NEW),
1867 "the new export serves the new publisher's media"
1868 );
1869 watcher.abort();
1870
1871 drop(new);
1872 }
1873
1874 #[tokio::test]
1880 async fn a_replacement_sibling_is_not_served_under_the_replaced_catalog() {
1881 const OLD: &[u8] = b"OLDOLDOLDOLDOLDO";
1882 const NEW: &[u8] = b"NEWNEWNEWNEWNEWN";
1883
1884 fn publish_media(
1889 origin: &moq_net::origin::Producer,
1890 payload: &'static [u8],
1891 recorder: Option<moq_mux::timeline::Recorder>,
1892 ) -> Box<dyn std::any::Any> {
1893 let broadcast = origin.create_broadcast("media").expect("publish allowed");
1894 broadcast.announce(Default::default()).expect("publish allowed");
1895 let track = broadcast.create_track("video0", None).unwrap();
1896
1897 let mut media = moq_mux::container::Producer::new(
1898 track,
1899 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
1900 );
1901 if let Some(recorder) = recorder {
1902 media = media.with_recorder(recorder);
1903 }
1904 media.write(payload_frame(0, true, payload)).unwrap();
1905 media.write(payload_frame(2_000_000, true, payload)).unwrap();
1906 media.write(payload_frame(4_000_000, true, payload)).unwrap();
1907
1908 Box::new((broadcast, media))
1909 }
1910
1911 let origin = produce_origin();
1912
1913 let mut live = origin.create_broadcast("live").expect("publish allowed");
1915 live.announce(Default::default()).expect("publish allowed");
1916 let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap();
1917 let recorder = catalog.enroll_test("video0").unwrap();
1918
1919 let old_media = publish_media(&origin, OLD, Some(recorder));
1920 settle().await;
1921
1922 let source = moq_mux::Source::new(origin.consume(), "live");
1923 let upstream = Upstream {
1924 broadcast: source.broadcast().await.unwrap(),
1925 source,
1926 };
1927
1928 let mut config = video_config();
1931 config.broadcast = Some(moq_net::path::Relative::new("media").to_owned());
1932
1933 let (rendition, watcher) = export(&upstream, &config);
1934 tokio::time::timeout(Duration::from_secs(5), rendition.playable())
1935 .await
1936 .expect("the timeline reaches the rendition");
1937
1938 drop(old_media);
1941 let new_media = publish_media(&origin, NEW, None);
1942 settle().await;
1943
1944 let served = rendition
1945 .segment(0)
1946 .await
1947 .expect("a replaced sibling is an unavailable group, not a server error");
1948 if let Some(served) = &served {
1949 assert!(
1950 !contains(served, NEW),
1951 "the replacement publisher's media was served under the replaced sibling's segment numbers"
1952 );
1953 }
1954 assert!(
1955 served.is_none(),
1956 "the replaced sibling broadcast is closed, so it serves nothing"
1957 );
1958 watcher.abort();
1959
1960 let source = moq_mux::Source::new(origin.consume(), "live");
1964 let fresh = Upstream {
1965 broadcast: source.broadcast().await.unwrap(),
1966 source,
1967 };
1968 let (rendition, watcher) = export(&fresh, &config);
1969 tokio::time::timeout(Duration::from_secs(5), rendition.playable())
1970 .await
1971 .expect("the timeline reaches the fresh rendition");
1972 let served = rendition
1973 .segment(0)
1974 .await
1975 .unwrap()
1976 .expect("the new publisher is servable on a freshly bound rendition");
1977 assert!(
1978 contains(&served, NEW),
1979 "the new export serves the new publisher's media"
1980 );
1981 watcher.abort();
1982
1983 drop((new_media, catalog, live));
1984 }
1985
1986 fn hops(ids: &[u64]) -> moq_net::Hops {
1987 let mut hops = moq_net::Hops::new();
1988 for id in ids {
1989 hops.push(moq_net::Hop::new(*id).unwrap()).unwrap();
1990 }
1991 hops
1992 }
1993
1994 fn sibling_route(first: u64) -> moq_net::origin::Route {
1995 moq_net::origin::Route::default().with_hops(hops(&[first]))
1996 }
1997
1998 fn write_routed_media(
2001 broadcast: &mut moq_net::broadcast::Producer,
2002 payload: &'static [u8],
2003 recorder: moq_mux::timeline::Recorder,
2004 start_micros: u64,
2005 ) -> moq_mux::container::Producer<moq_mux::catalog::hang::Container> {
2006 let track = broadcast.create_track("video0", None).unwrap();
2007 let mut media = moq_mux::container::Producer::new(
2008 track,
2009 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
2010 )
2011 .with_recorder(recorder);
2012 media.write(payload_frame(start_micros, true, payload)).unwrap();
2013 media
2014 .write(payload_frame(start_micros + 2_000_000, true, payload))
2015 .unwrap();
2016 media
2017 .write(payload_frame(start_micros + 4_000_000, true, payload))
2018 .unwrap();
2019 media
2020 }
2021
2022 async fn accept_sibling(server: &moq_net::origin::Dynamic, broadcast: &moq_net::broadcast::Producer) {
2023 let request = tokio::time::timeout(Duration::from_secs(5), server.requested_broadcast())
2024 .await
2025 .expect("the sibling path is requested")
2026 .expect("the handler is open");
2027 request.accept(broadcast);
2028 }
2029
2030 async fn until_empty(rendition: &Rendition) {
2031 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
2032 loop {
2033 if rendition.snapshot().segments.is_empty() {
2034 return;
2035 }
2036 assert!(
2037 tokio::time::Instant::now() < deadline,
2038 "old sibling rows were not dropped after replacement"
2039 );
2040 tokio::task::yield_now().await;
2041 }
2042 }
2043
2044 #[tokio::test]
2049 async fn a_replaced_first_hop_sibling_drops_old_rows_and_rebinds() {
2050 const OLD: &[u8] = b"OLDOLDOLDOLDOLDO";
2051 const NEW: &[u8] = b"NEWNEWNEWNEWNEWN";
2052
2053 let origin = produce_origin();
2054 let mut live = origin.create_broadcast("live").expect("publish allowed");
2055 live.announce(Default::default()).expect("publish allowed");
2056 let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap();
2057 let recorder = catalog.enroll_test("video0").unwrap();
2058
2059 let mut old_media = moq_net::broadcast::Info::new().produce();
2060 let _old_track = write_routed_media(&mut old_media, OLD, recorder, 0);
2061 let old_server = origin.dynamic("media", sibling_route(10)).unwrap();
2062 settle().await;
2063
2064 let source = moq_mux::Source::new(origin.consume(), "live");
2065 let upstream = Upstream {
2066 broadcast: source.broadcast().await.unwrap(),
2067 source,
2068 };
2069 let mut config = video_config();
2070 config.broadcast = Some(moq_net::path::Relative::new("media").to_owned());
2071
2072 let (rendition, watcher) = export(&upstream, &config);
2073 accept_sibling(&old_server, &old_media).await;
2074 tokio::time::timeout(Duration::from_secs(5), rendition.playable())
2075 .await
2076 .expect("the timeline reaches the rendition");
2077 let served = rendition
2078 .segment(0)
2079 .await
2080 .unwrap()
2081 .expect("the original sibling is servable");
2082 assert!(contains(&served, OLD), "the first hop serves the original publisher");
2083 assert!(!rendition.snapshot().segments.is_empty());
2084
2085 let new_server = origin.dynamic("media", sibling_route(11)).unwrap();
2088 drop((old_server, old_media, _old_track));
2089 until_empty(&rendition).await;
2090 assert!(
2091 rendition.segment(0).await.unwrap().is_none(),
2092 "a row listed for the old publisher answers 404 after replacement"
2093 );
2094
2095 let mut new_media = moq_net::broadcast::Info::new().produce();
2096 accept_sibling(&new_server, &new_media).await;
2097 tokio::time::timeout(Duration::from_secs(5), origin.consume().request_broadcast("media"))
2100 .await
2101 .expect("the rebound sibling resolves")
2102 .expect("the replacement is routable");
2103 let recorder = catalog.enroll_test("video0").unwrap();
2104 let _new_track = write_routed_media(&mut new_media, NEW, recorder, 6_000_000);
2105 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
2106 loop {
2107 if !rendition.snapshot().segments.is_empty() {
2108 break;
2109 }
2110 assert!(
2111 tokio::time::Instant::now() < deadline,
2112 "rows produced after the new bind were not listed"
2113 );
2114 tokio::task::yield_now().await;
2115 }
2116 let listed = rendition
2117 .snapshot()
2118 .segments
2119 .into_iter()
2120 .find(|segment| !segment.gap)
2121 .expect("a non-gap row after the new bind")
2122 .segment;
2123 let served = rendition
2124 .segment(listed)
2125 .await
2126 .unwrap()
2127 .expect("the rebound sibling is servable");
2128 assert!(
2129 contains(&served, NEW),
2130 "rows after the new bind come from the replacement"
2131 );
2132 assert!(!contains(&served, OLD));
2133 assert!(
2134 rendition.segment(0).await.unwrap().is_none(),
2135 "a row listed for the old publisher is not served from the replacement"
2136 );
2137 watcher.abort();
2138 drop((new_media, _new_track, new_server, catalog, live));
2139 }
2140
2141 #[tokio::test]
2144 async fn a_segment_straddling_a_restart_keeps_its_init_out_of_the_new_run() {
2145 tokio::time::pause();
2146 const KEYFRAME: &[u8] = &[0x10, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0x40, 0x01, 0xf0, 0x00];
2148
2149 let origin = produce_origin();
2150 let mut live = origin.create_broadcast("live").expect("publish allowed");
2151 live.announce(Default::default()).expect("publish allowed");
2152 let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap();
2153 let recorder = catalog.enroll_test("video0").unwrap();
2154
2155 let mut media = moq_net::broadcast::Info::new().produce();
2156 let _track = write_routed_media(&mut media, KEYFRAME, recorder, 0);
2157 let server = origin.dynamic("media", sibling_route(10)).unwrap();
2158 settle().await;
2159
2160 let source = moq_mux::Source::new(origin.consume(), "live");
2161 let upstream = Upstream {
2162 broadcast: source.broadcast().await.unwrap(),
2163 source,
2164 };
2165 let mut config = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8);
2166 config.broadcast = Some(moq_net::path::Relative::new("media").to_owned());
2167
2168 let mut snapshot = moq_mux::catalog::hang::Catalog::default();
2169 snapshot.video.renditions.insert("video0".to_string(), config);
2170 let archive = hang::catalog::Archive::new(hang::timeline::DEFAULT_NAME);
2171 snapshot.archive = Some(archive.clone());
2172 let renditions = renditions::Producer::new(Config::default().window);
2173 renditions.sync(&upstream, &snapshot);
2174 let fanout = renditions.fanout();
2175 let watcher = tokio::spawn(watch_timeline(upstream.broadcast.clone(), archive, fanout.clone()));
2176 let rendition = renditions.get(Kind::Video, "video0").unwrap();
2177 tokio::time::timeout(Duration::from_secs(5), rendition.playable())
2178 .await
2179 .expect("the timeline reaches the rendition");
2180
2181 let mut segment = std::pin::pin!(rendition.segment(0));
2183 assert!(
2184 tokio::time::timeout(Duration::from_millis(10), segment.as_mut())
2185 .await
2186 .is_err()
2187 );
2188 fanout.set_generation(Some("run-1".into()));
2189 fanout.set_generation(Some("run-2".into()));
2190
2191 accept_sibling(&server, &media).await;
2192 segment
2193 .await
2194 .unwrap()
2195 .expect("the old run's rows were captured before the restart");
2196 assert!(
2197 rendition.init().await.unwrap().is_none(),
2198 "the new run has no media of its own yet"
2199 );
2200 watcher.abort();
2201 }
2202
2203 #[tokio::test]
2206 async fn a_sibling_present_at_export_start_is_served() {
2207 const NEW: &[u8] = b"NEWNEWNEWNEWNEWN";
2208
2209 let origin = produce_origin();
2210 let mut live = origin.create_broadcast("live").expect("publish allowed");
2211 live.announce(Default::default()).expect("publish allowed");
2212 let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap();
2213 let recorder = catalog.enroll_test("video0").unwrap();
2214
2215 let mut media = moq_net::broadcast::Info::new().produce();
2216 let _track = write_routed_media(&mut media, NEW, recorder, 0);
2217 let server = origin.dynamic("media", sibling_route(11)).unwrap();
2218 settle().await;
2219
2220 let source = moq_mux::Source::new(origin.consume(), "live");
2221 let upstream = Upstream {
2222 broadcast: source.broadcast().await.unwrap(),
2223 source,
2224 };
2225 let mut config = video_config();
2226 config.broadcast = Some(moq_net::path::Relative::new("media").to_owned());
2227
2228 let (rendition, watcher) = export(&upstream, &config);
2229 accept_sibling(&server, &media).await;
2230 tokio::time::timeout(Duration::from_secs(5), rendition.playable())
2231 .await
2232 .expect("the timeline reaches the rendition");
2233 let served = rendition
2234 .segment(0)
2235 .await
2236 .unwrap()
2237 .expect("the sibling present at export start is servable");
2238 assert!(contains(&served, NEW));
2239 watcher.abort();
2240 drop((media, _track, server, catalog, live));
2241 }
2242
2243 #[tokio::test]
2247 async fn a_replaced_sibling_recovers_after_an_unroutable_gap() {
2248 const OLD: &[u8] = b"OLDOLDOLDOLDOLDO";
2249 const NEW: &[u8] = b"NEWNEWNEWNEWNEWN";
2250
2251 let origin = produce_origin();
2252 let mut live = origin.create_broadcast("live").expect("publish allowed");
2253 live.announce(Default::default()).expect("publish allowed");
2254 let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap();
2255 let recorder = catalog.enroll_test("video0").unwrap();
2256
2257 let mut old_media = moq_net::broadcast::Info::new().produce();
2258 let _old_track = write_routed_media(&mut old_media, OLD, recorder, 0);
2259 let old_server = origin.dynamic("media", sibling_route(10)).unwrap();
2260 settle().await;
2261
2262 let source = moq_mux::Source::new(origin.consume(), "live");
2263 let upstream = Upstream {
2264 broadcast: source.broadcast().await.unwrap(),
2265 source,
2266 };
2267 let mut config = video_config();
2268 config.broadcast = Some(moq_net::path::Relative::new("media").to_owned());
2269
2270 let (rendition, watcher) = export(&upstream, &config);
2271 accept_sibling(&old_server, &old_media).await;
2272 tokio::time::timeout(Duration::from_secs(5), rendition.playable())
2273 .await
2274 .expect("the timeline reaches the rendition");
2275 assert!(rendition.segment(0).await.unwrap().is_some());
2276
2277 drop((old_server, old_media, _old_track));
2278 until_empty(&rendition).await;
2279 for _ in 0..4 {
2280 assert!(rendition.snapshot().segments.is_empty());
2281 assert!(rendition.segment(0).await.unwrap().is_none());
2282 }
2283
2284 let new_server = origin.dynamic("media", sibling_route(11)).unwrap();
2285 let mut new_media = moq_net::broadcast::Info::new().produce();
2286 let _ = rendition.snapshot();
2287 accept_sibling(&new_server, &new_media).await;
2288 tokio::time::timeout(Duration::from_secs(5), origin.consume().request_broadcast("media"))
2289 .await
2290 .expect("the rebound sibling resolves")
2291 .expect("the replacement is routable");
2292 let recorder = catalog.enroll_test("video0").unwrap();
2293 let _new_track = write_routed_media(&mut new_media, NEW, recorder, 6_000_000);
2294 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
2295 loop {
2296 if !rendition.snapshot().segments.is_empty() {
2297 break;
2298 }
2299 assert!(
2300 tokio::time::Instant::now() < deadline,
2301 "rows produced after the new bind were not listed"
2302 );
2303 tokio::task::yield_now().await;
2304 }
2305 let listed = rendition
2306 .snapshot()
2307 .segments
2308 .into_iter()
2309 .find(|segment| !segment.gap)
2310 .expect("a non-gap row after the new bind")
2311 .segment;
2312 let served = rendition
2313 .segment(listed)
2314 .await
2315 .unwrap()
2316 .expect("the rebound sibling is servable after an unroutable gap");
2317 assert!(contains(&served, NEW));
2318 assert!(rendition.segment(0).await.unwrap().is_none());
2319 watcher.abort();
2320 drop((new_media, _new_track, new_server, catalog, live));
2321 }
2322
2323 #[tokio::test]
2327 async fn removing_a_rendition_ends_its_segment_cursor() {
2328 let origin = produce_origin();
2329 let broadcast = origin.create_broadcast("live").expect("publish allowed");
2330 broadcast.announce(Default::default()).expect("publish allowed");
2331 let source = moq_mux::Source::new(origin.consume(), "live");
2332 settle().await;
2333 let upstream = Upstream {
2334 broadcast: source.broadcast().await.unwrap(),
2335 source,
2336 };
2337
2338 let renditions = renditions::Producer::new(Config::default().window);
2340 let media = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8);
2341 let mut catalog = moq_mux::catalog::hang::Catalog::default();
2342 catalog.video.renditions.insert("video".to_string(), media);
2343 catalog.archive = Some(hang::catalog::Archive::new(hang::timeline::DEFAULT_NAME));
2344 renditions.sync(&upstream, &catalog);
2345
2346 let rendition = renditions.get(Kind::Video, "video").expect("rendition synced");
2347 let mut segments = rendition.segments();
2348
2349 let mut empty = moq_mux::catalog::hang::Catalog::default();
2351 empty.archive = Some(hang::catalog::Archive::new(hang::timeline::DEFAULT_NAME));
2352 renditions.sync(&upstream, &empty);
2353 let ended = tokio::time::timeout(Duration::from_secs(5), segments.next())
2354 .await
2355 .expect("a removed rendition's cursor ends instead of parking")
2356 .unwrap();
2357 assert!(ended.is_none(), "no segments remained to drain");
2358
2359 drop((broadcast, rendition));
2360 }
2361
2362 #[tokio::test]
2367 async fn dropping_the_broadcaster_releases_its_renditions() {
2368 let origin = produce_origin();
2369 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
2370 broadcast.announce(Default::default()).expect("publish allowed");
2371 settle().await;
2372 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
2373
2374 let reserved = catalog.reserve();
2375 let mut registration = catalog.test_video("video0").unwrap();
2376 let config = video_config();
2377 registration.set(config).unwrap();
2378 drop(reserved);
2379
2380 let track = broadcast.create_track("video0", None).unwrap();
2381 let mut media = catalog
2382 .media_producer(
2383 track,
2384 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
2385 )
2386 .unwrap();
2387 media.write(frame(0, true)).unwrap();
2388 media.write(frame(2_000_000, true)).unwrap();
2389
2390 let source = moq_mux::Source::new(origin.consume(), "live");
2391 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
2392
2393 let mut renditions = broadcaster.renditions();
2394 let added = tokio::time::timeout(Duration::from_secs(5), renditions.next())
2395 .await
2396 .expect("a rendition is discovered");
2397 let Some(renditions::Event::Added(_rendition)) = added else {
2398 panic!("expected an Added event");
2399 };
2400
2401 drop(broadcaster);
2403
2404 let next = tokio::time::timeout(Duration::from_secs(5), renditions.next())
2405 .await
2406 .expect("the cursor resolves after the broadcaster drops");
2407 assert!(
2408 matches!(next, Some(renditions::Event::Removed { .. })),
2409 "dropping the broadcaster releases its renditions to a live cursor"
2410 );
2411
2412 let ended = tokio::time::timeout(Duration::from_secs(5), renditions.next())
2415 .await
2416 .expect("the cursor terminates after the broadcaster drops");
2417 assert!(ended.is_none(), "the cursor ends once the broadcaster is gone");
2418
2419 drop((catalog, media, registration, broadcast));
2420 }
2421
2422 #[tokio::test]
2426 async fn record_cursors_yield_renditions_and_segments() {
2427 let origin = produce_origin();
2428 let mut broadcast = origin.create_broadcast("live").expect("publish allowed");
2429 broadcast.announce(Default::default()).expect("publish allowed");
2430 settle().await;
2431 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
2432
2433 let reserved = catalog.reserve();
2434 let mut registration = catalog.test_video("video0").unwrap();
2435 let config = video_config();
2436 registration.set(config).unwrap();
2437 drop(reserved);
2438
2439 let track = broadcast.create_track("video0", None).unwrap();
2441 let mut media = catalog
2442 .media_producer(
2443 track,
2444 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
2445 )
2446 .unwrap();
2447 media.write(frame(0, true)).unwrap();
2448 media.write(frame(2_000_000, true)).unwrap();
2449 media.write(frame(4_000_000, true)).unwrap();
2450
2451 let source = moq_mux::Source::new(origin.consume(), "live");
2452 let broadcaster = Broadcaster::new(source, Config::default()).await.unwrap();
2453
2454 let mut renditions = broadcaster.renditions();
2455 let rendition = match tokio::time::timeout(Duration::from_secs(5), renditions.next())
2456 .await
2457 .expect("a rendition is discovered")
2458 {
2459 Some(renditions::Event::Added(rendition)) => rendition,
2460 other => panic!("expected an Added event, got {:?}", other.is_some()),
2461 };
2462 assert_eq!(rendition.kind, Kind::Video);
2463 assert_eq!(rendition.name, "video0");
2464
2465 let mut segments = rendition.segments();
2466 assert!(segments.init().await.unwrap().is_some(), "init is buildable");
2467
2468 let first = tokio::time::timeout(Duration::from_secs(5), segments.next())
2469 .await
2470 .expect("first segment finalizes")
2471 .unwrap()
2472 .expect("a segment, not end");
2473 assert_eq!(first.segment, 0);
2474 assert_eq!(&first.media[4..8], b"moof", "the segment carries its transmuxed media");
2475 assert_eq!(first.duration, Duration::from_secs(2));
2476 assert!(!first.discontinuity, "a clean start is not a discontinuity");
2477
2478 let second = segments.next().await.unwrap().expect("second segment");
2479 assert_eq!(second.segment, 1);
2480 assert!(!second.discontinuity, "consecutive segments are continuous");
2481
2482 drop((catalog, media, registration));
2488 broadcast.close();
2489
2490 let end = tokio::time::timeout(Duration::from_secs(5), segments.next())
2491 .await
2492 .expect("segment cursor resolves after the source is lost")
2493 .unwrap();
2494 assert!(
2495 end.is_none(),
2496 "an abrupt end does not finalize the open live-edge group"
2497 );
2498
2499 let ended = tokio::time::timeout(Duration::from_secs(5), renditions.next())
2501 .await
2502 .expect("renditions cursor resolves");
2503 assert!(ended.is_none(), "renditions cursor ends when the broadcast closes");
2504 }
2505}