1use std::sync::{Arc, Mutex, RwLock};
5use std::task::Poll;
6use std::time::{Duration, SystemTime};
7
8use bytes::Bytes;
9use hang::catalog::{Archive, AudioConfig, Clock, VideoConfig};
10use moq_mux::container::fmp4::Muxer;
11use moq_mux::timeline::Entry;
12use sha2::{Digest, Sha256};
13
14use super::playlist::{Segment, Snapshot};
15use super::upstream::Upstream;
16use super::{mpd, segments};
17use crate::Result;
18
19const DEFAULT_VIDEO_BITRATE: u64 = 2_000_000;
21const DEFAULT_AUDIO_BITRATE: u64 = 128_000;
22
23fn default_bandwidth(kind: Kind) -> u64 {
25 match kind {
26 Kind::Video => DEFAULT_VIDEO_BITRATE,
27 Kind::Audio => DEFAULT_AUDIO_BITRATE,
28 }
29}
30
31const MAX_SEGMENT_GROUPS: u64 = 1024;
34
35#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
40pub enum Kind {
41 Video,
43 Audio,
45}
46
47impl Kind {
48 pub fn parse(value: &str) -> Option<Self> {
50 match value {
51 "video" => Some(Self::Video),
52 "audio" => Some(Self::Audio),
53 _ => None,
54 }
55 }
56
57 pub fn as_str(self) -> &'static str {
59 match self {
60 Kind::Video => "video",
61 Kind::Audio => "audio",
62 }
63 }
64}
65
66const FEED_MAX_DELAY: Duration = Duration::from_secs(30);
71
72struct Init {
77 bytes: Bytes,
78 hash: String,
79}
80
81impl Init {
82 fn new(bytes: Bytes) -> Self {
83 let hash = Sha256::digest(&bytes)[..8].iter().map(|b| format!("{b:02x}")).collect();
85 Self { bytes, hash }
86 }
87}
88
89#[derive(Clone, Default)]
93struct Run {
94 generation: Option<Arc<str>>,
95 init: Option<Arc<Init>>,
96 epoch: u64,
98}
99
100enum Config {
102 Video(VideoConfig),
103 Audio(AudioConfig),
104}
105
106struct Media {
108 handle: Mutex<Handle>,
109 sibling: Option<(moq_mux::Source, moq_net::path::RelativeOwned)>,
112}
113
114struct Handle {
115 binding: Arc<moq_mux::Binding>,
116 waiting: bool,
118}
119
120impl Media {
121 fn bind(upstream: &Upstream, rel: Option<&moq_net::path::RelativeOwned>) -> moq_mux::Result<Self> {
122 Ok(Self {
123 handle: Mutex::new(Handle {
124 binding: Arc::new(upstream.bind(rel)?),
125 waiting: false,
126 }),
127 sibling: sibling(upstream, rel),
128 })
129 }
130
131 fn admits(&self, window: &segments::Producer) -> bool {
134 self.sync(window);
135 !self.handle.lock().expect("media lock poisoned").waiting
136 }
137
138 fn sync(&self, window: &segments::Producer) -> Arc<moq_mux::Binding> {
142 let Some((source, rel)) = &self.sibling else {
143 return self.handle.lock().expect("media lock poisoned").binding.clone();
144 };
145 let mut handle = self.handle.lock().expect("media lock poisoned");
146 match handle.binding.poll_broadcast(&kio::Waiter::noop()) {
147 Poll::Ready(Ok(broadcast)) if broadcast.is_closed() => {
148 window.clear();
152 if let Ok(next) = source.bind(Some(rel)) {
153 handle.binding = Arc::new(next);
154 }
155 handle.waiting = true;
156 }
157 Poll::Ready(Ok(_)) => handle.waiting = false,
158 Poll::Ready(Err(_)) => {
159 if let Ok(next) = source.bind(Some(rel)) {
164 handle.binding = Arc::new(next);
165 }
166 }
167 Poll::Pending => {}
168 }
169 handle.binding.clone()
170 }
171}
172
173fn sibling(
175 upstream: &Upstream,
176 rel: Option<&moq_net::path::RelativeOwned>,
177) -> Option<(moq_mux::Source, moq_net::path::RelativeOwned)> {
178 let target = upstream.source.resolve_reference(rel)?;
179 if upstream.source.resolve_reference(None).as_ref() == Some(&target) {
180 return None;
181 }
182 Some((
183 upstream.source.clone(),
184 rel.cloned().unwrap_or_else(moq_net::path::Relative::empty),
185 ))
186}
187
188fn normalize_video(config: &VideoConfig) -> VideoConfig {
195 let mut config = config.clone();
196 config.bitrate = None;
197 config.jitter = None;
198 config.delay = None;
199 config.label = None;
200 config.stalled = None;
201 config.framerate = None;
204 config
205}
206
207fn normalize_audio(config: &AudioConfig) -> AudioConfig {
209 let mut config = config.clone();
210 config.bitrate = None;
211 config.jitter = None;
212 config.delay = None;
213 config.label = None;
214 config
215}
216
217pub struct Rendition {
220 pub name: String,
222 pub kind: Kind,
224 bitrate: RwLock<Option<u64>>,
227 framerate: RwLock<Option<f64>>,
230 pub width: Option<u32>,
232 pub height: Option<u32>,
234 pub codec: String,
236
237 config: Config,
238 section: Archive,
240 clock: Option<Clock>,
243 live: Arc<segments::Producer>,
246 media: Media,
249 run: Mutex<Run>,
255 building: tokio::sync::Mutex<()>,
257}
258
259impl Rendition {
260 pub(crate) fn matches_video(&self, config: &VideoConfig) -> bool {
265 let Config::Video(current) = &self.config else {
266 return false;
267 };
268 normalize_video(current) == normalize_video(config)
269 }
270
271 pub(crate) fn matches_audio(&self, config: &AudioConfig) -> bool {
273 let Config::Audio(current) = &self.config else {
274 return false;
275 };
276 normalize_audio(current) == normalize_audio(config)
277 }
278
279 pub fn bandwidth(&self) -> u64 {
282 self.bitrate().unwrap_or(default_bandwidth(self.kind))
283 }
284
285 pub(crate) fn refresh(&self, bitrate: Option<u64>, framerate: Option<f64>) {
292 *self.bitrate.write().expect("bitrate lock poisoned") = bitrate;
293 *self.framerate.write().expect("framerate lock poisoned") = framerate;
294 }
295
296 fn bitrate(&self) -> Option<u64> {
298 *self.bitrate.read().expect("bitrate lock poisoned")
299 }
300
301 fn framerate(&self) -> Option<f64> {
303 *self.framerate.read().expect("framerate lock poisoned")
304 }
305
306 pub(crate) fn video(
312 name: String,
313 config: &VideoConfig,
314 upstream: &Upstream,
315 section: Archive,
316 clock: Option<Clock>,
317 ) -> moq_mux::Result<Self> {
318 Ok(Self {
319 name,
320 kind: Kind::Video,
321 bitrate: RwLock::new(config.bitrate),
322 framerate: RwLock::new(config.framerate),
323 width: config.coded_width,
324 height: config.coded_height,
325 codec: config.codec.to_string(),
326 config: Config::Video(config.clone()),
327 section,
328 clock,
329 live: Arc::new(segments::Producer::new()),
330 media: Media::bind(upstream, config.broadcast.as_ref())?,
331 run: Mutex::default(),
332 building: tokio::sync::Mutex::new(()),
333 })
334 }
335
336 pub(crate) fn audio(
339 name: String,
340 config: &AudioConfig,
341 upstream: &Upstream,
342 section: Archive,
343 clock: Option<Clock>,
344 ) -> moq_mux::Result<Self> {
345 Ok(Self {
346 name,
347 kind: Kind::Audio,
348 bitrate: RwLock::new(config.bitrate),
349 framerate: RwLock::new(None),
350 width: None,
351 height: None,
352 codec: config.codec.to_string(),
353 config: Config::Audio(config.clone()),
354 section,
355 clock,
356 live: Arc::new(segments::Producer::new()),
357 media: Media::bind(upstream, config.broadcast.as_ref())?,
358 run: Mutex::default(),
359 building: tokio::sync::Mutex::new(()),
360 })
361 }
362
363 pub(crate) fn push(&self, index: u64, entry: &Entry, discontinuity: u64, window: Duration) {
366 if !self.media.admits(&self.live) {
367 return;
368 }
369 let row = segments::Row {
370 index,
371 segment: entry.segment,
372 ranges: entry.tracks.get(&self.name).cloned().unwrap_or_default(),
373 duration: entry.duration,
374 pts: entry.pts,
375 end: Duration::from(entry.pts) + entry.duration,
376 discontinuity,
377 };
378 self.live.push(row, window);
379 }
380
381 pub(crate) fn pop(&self, range: std::ops::Range<u64>) {
383 self.live.pop(range);
384 }
385
386 pub(crate) fn clear(&self) {
388 self.live.clear();
389 }
390
391 pub(crate) fn label(&self, generation: Option<Arc<str>>) {
393 self.run.lock().expect("run lock poisoned").generation = generation;
394 }
395
396 pub(crate) fn restart(&self, generation: Option<Arc<str>>) {
401 self.live.clear();
402 let mut run = self.run.lock().expect("run lock poisoned");
403 *run = Run {
404 generation,
405 init: None,
406 epoch: run.epoch + 1,
407 };
408 }
409
410 fn run(&self) -> Run {
411 self.run.lock().expect("run lock poisoned").clone()
412 }
413
414 #[cfg_attr(not(feature = "server"), allow(dead_code))]
416 pub(crate) fn generation(&self) -> Option<Arc<str>> {
417 self.run().generation
418 }
419
420 pub(crate) fn end(&self) {
422 self.live.end();
423 }
424
425 pub fn segments(self: &Arc<Self>) -> segments::Consumer {
428 self.live.subscribe(self.clone())
429 }
430
431 pub(crate) fn close(&self) {
437 self.live.close();
438 }
439
440 pub async fn playable(&self) {
444 kio::wait(|waiter| self.poll_playable(waiter)).await;
445 }
446
447 pub(crate) fn poll_playable(&self, waiter: &kio::Waiter) -> Poll<()> {
449 self.media.sync(&self.live);
450 self.live.poll_playable(waiter)
451 }
452
453 pub fn media_playlist(&self, query: Option<&str>) -> Option<String> {
468 let run = self.run();
470 let init = self.built_init(&run)?;
471 self.is_playable()
472 .then(|| super::render_media(&self.snapshot_as(run.generation), &init.hash, query))
473 }
474
475 pub async fn playlist(&self, query: Option<&str>) -> Result<Option<String>> {
482 self.playable().await;
483 if self.init().await?.is_none() {
486 return Ok(None);
487 }
488 Ok(self.media_playlist(query))
489 }
490
491 #[cfg(test)]
493 pub(crate) fn snapshot(&self) -> Snapshot {
494 self.snapshot_as(self.generation())
495 }
496
497 fn snapshot_as(&self, generation: Option<Arc<str>>) -> Snapshot {
499 self.media.sync(&self.live);
500 let window = self.live.window();
501
502 let declared = self
507 .section
508 .duration_max
509 .map(|max| max.div_ceil((self.section.timescale as u64).max(1)))
510 .unwrap_or(0);
511
512 let observed = window
516 .segments
517 .iter()
518 .map(|s| s.duration.as_secs_f64().ceil() as u64)
519 .max()
520 .unwrap_or(0);
521 let target_duration = declared.max(observed).max(1);
522
523 let program_date_time = window.segments.first().and_then(|first| self.wall_clock(first.pts));
524
525 let mut previous: Option<u64> = None;
526 let segments = window
527 .segments
528 .into_iter()
529 .map(|s| {
530 let discontinuity = previous.replace(s.discontinuity).is_some_and(|p| p != s.discontinuity);
531 Segment {
532 segment: s.segment,
533 duration: s.duration,
534 gap: s.ranges.is_empty(),
535 discontinuity,
536 }
537 })
538 .collect();
539
540 Snapshot {
541 target_duration,
542 media_sequence: window.sequence,
543 segments,
544 finished: window.ended,
545 program_date_time,
546 generation,
547 }
548 }
549
550 pub(crate) fn is_playable(&self) -> bool {
553 self.media.sync(&self.live);
554 self.live.is_playable()
555 }
556
557 pub(crate) fn representation(&self) -> Option<mpd::Representation> {
562 let run = self.run();
564 let init = self.built_init(&run)?;
565 self.media.sync(&self.live);
566 let window = self.live.window();
567 let timescale = self.timescale();
568 let segments = window
569 .segments
570 .iter()
571 .filter(|row| !row.ranges.is_empty())
572 .filter_map(|row| {
573 let t = row.pts.as_scale(timescale) as u64;
574 let span = row.end.saturating_sub(row.pts.into());
575 let d = (span.as_nanos() * timescale.as_u64() as u128 / 1_000_000_000) as u64;
576 (d > 0).then_some((t, d))
582 })
583 .collect();
584 let (framerate, sample_rate, channel_count) = match &self.config {
585 Config::Video(_) => (self.framerate(), None, None),
586 Config::Audio(config) => (None, Some(config.sample_rate), Some(config.channel_count)),
587 };
588 Some(mpd::Representation {
589 name: self.name.clone(),
590 kind: self.kind,
591 bandwidth: self.bandwidth(),
592 codec: self.codec.clone(),
593 width: self.width,
594 height: self.height,
595 framerate,
596 sample_rate,
597 channel_count,
598 timescale: self.section.timescale.max(1),
599 segments,
600 ended: window.ended,
601 init: init.hash.clone(),
602 generation: run.generation,
603 })
604 }
605
606 #[cfg_attr(not(feature = "server"), allow(dead_code))]
610 pub(crate) async fn segment_at(&self, time: u64) -> Result<Option<Bytes>> {
611 self.media.sync(&self.live);
612 let Some(segment) = self.live.segment_number_at(time, self.timescale()) else {
613 return Ok(None);
614 };
615 self.segment(segment).await
616 }
617
618 fn timescale(&self) -> moq_net::Timescale {
620 moq_net::Timescale::new((self.section.timescale as u64).max(1)).expect("clamped nonzero timescale")
621 }
622
623 pub(crate) fn wall_clock(&self, pts: moq_net::Timestamp) -> Option<SystemTime> {
627 let clock = self.clock.as_ref()?;
628 clock.wall_clock(pts).ok()
629 }
630
631 pub(crate) fn muxer(&self) -> Result<Muxer> {
634 let bitrate = self.bitrate();
635 Ok(match &self.config {
636 Config::Video(config) => {
637 let mut config = config.clone();
638 config.bitrate = bitrate;
639 Muxer::video(&config)?.with_framerate(self.framerate())
640 }
641 Config::Audio(config) => {
642 let mut config = config.clone();
643 config.bitrate = bitrate;
644 Muxer::audio(&config)?
645 }
646 })
647 }
648
649 pub(crate) fn binding(&self) -> Arc<moq_mux::Binding> {
651 self.media.sync(&self.live)
652 }
653
654 pub(crate) fn poll_subscribe(
656 &self,
657 binding: &moq_mux::Binding,
658 waiter: &kio::Waiter,
659 ) -> Poll<Option<moq_net::track::Subscribing>> {
660 let Ok(broadcast) = std::task::ready!(binding.poll_broadcast(waiter)) else {
661 return Poll::Ready(None);
662 };
663 let track = broadcast.track(&self.name).ok();
664 let subscription = moq_net::track::Subscription::default().with_max_age(FEED_MAX_DELAY);
665 Poll::Ready(track.map(|track| track.subscribe(subscription).into_inner()))
666 }
667
668 async fn track(
676 &self,
677 binding: &moq_mux::Binding,
678 ) -> Option<(moq_net::broadcast::Consumer, moq_net::track::Consumer)> {
679 let broadcast = binding.broadcast().await.ok()?;
680 let track = broadcast.track(&self.name).ok()?;
681 Some((broadcast, track))
682 }
683
684 pub async fn init(&self) -> Result<Option<Bytes>> {
689 Ok(self.load_init().await?.map(|init| init.bytes.clone()))
690 }
691
692 #[cfg_attr(not(feature = "server"), allow(dead_code))]
695 pub(crate) async fn init_versioned(&self, hash: &str) -> Result<Option<Bytes>> {
696 let init = self.load_init().await?;
697 Ok(init.filter(|init| init.hash == hash).map(|init| init.bytes.clone()))
698 }
699
700 fn built_init(&self, run: &Run) -> Option<Arc<Init>> {
703 if let Some(init) = &run.init {
704 return Some(init.clone());
705 }
706 let bytes = self.muxer().ok()?.init().ok()??;
708 Some(self.cache_init(run.epoch, bytes))
709 }
710
711 fn cache_init(&self, epoch: u64, bytes: Bytes) -> Arc<Init> {
714 let mut run = self.run.lock().expect("run lock poisoned");
715 if run.epoch != epoch {
716 return Arc::new(Init::new(bytes));
717 }
718 run.init.get_or_insert_with(|| Arc::new(Init::new(bytes))).clone()
719 }
720
721 async fn load_init(&self) -> Result<Option<Arc<Init>>> {
722 let binding = self.media.sync(&self.live);
723 if let Some(init) = self.run().init {
726 return Ok(Some(init));
727 }
728 let _building = self.building.lock().await;
729 let run = self.run();
730 if let Some(init) = run.init {
731 return Ok(Some(init));
732 }
733
734 let mut muxer = self.muxer()?;
735
736 let bytes = match muxer.init()? {
739 Some(bytes) => bytes,
740 None => {
741 let Some(sequence) = self.live.latest_keyframe_group() else {
742 return Ok(None);
743 };
744 let Some((broadcast, track)) = self.track(&binding).await else {
745 return Ok(None);
746 };
747 let Ok(mut group) = fetch(&track, sequence, &broadcast).await? else {
748 return Ok(None);
749 };
750 if read_group(&mut muxer, &mut group, &broadcast).await?.is_err() {
752 return Ok(None);
753 }
754 let Some(bytes) = muxer.init()? else {
755 return Ok(None);
756 };
757 bytes
758 }
759 };
760 Ok(Some(self.cache_init(run.epoch, bytes)))
761 }
762
763 pub async fn segment(&self, segment: u64) -> Result<Option<Bytes>> {
770 let binding = self.media.sync(&self.live);
771 let run = self.run();
774 let Some(ranges) = self.live.segment_ranges(segment) else {
775 return Ok(None);
776 };
777 if ranges.is_empty() {
778 return Ok(None);
780 }
781
782 let total: u64 = ranges
786 .iter()
787 .map(|r| r.end.saturating_sub(r.start).saturating_add(1))
788 .sum();
789 if total > MAX_SEGMENT_GROUPS {
790 tracing::warn!(
791 total,
792 "segment spans more than MAX_SEGMENT_GROUPS; refusing to truncate"
793 );
794 return Ok(None);
795 }
796
797 let Some((broadcast, track)) = self.track(&binding).await else {
798 return Ok(None);
799 };
800
801 let mut muxer = self.muxer()?;
802 let mut frames = Vec::new();
807 for range in &ranges {
808 for sequence in range.start..=range.end {
809 let miss = match fetch(&track, sequence, &broadcast).await? {
810 Ok(mut group) => match read_group(&mut muxer, &mut group, &broadcast).await? {
811 Ok(mut group_frames) => {
812 frames.append(&mut group_frames);
813 continue;
814 }
815 Err(err) => err,
816 },
817 Err(err) => err,
818 };
819 tracing::warn!(
820 segment,
821 group = sequence,
822 err = %miss,
823 "abandoning an advertised segment: a group is unavailable"
824 );
825 return Ok(None);
826 }
827 }
828
829 if frames.is_empty() {
830 return Ok(None);
831 }
832 let fragment = muxer.fragment(segment as u32, &frames)?;
833 if let Some(bytes) = muxer.init()? {
836 self.cache_init(run.epoch, bytes);
837 }
838 Ok(Some(fragment))
839 }
840}
841
842async fn fetch(
845 track: &moq_net::track::Consumer,
846 sequence: u64,
847 broadcast: &moq_net::broadcast::Consumer,
848) -> Result<std::result::Result<moq_net::group::Consumer, moq_net::Error>> {
849 match track.fetch_group(sequence, None).await {
850 Ok(group) => Ok(Ok(group)),
851 Err(err) if is_unservable(&err, broadcast) => Ok(Err(err)),
852 Err(err) => Err(err.into()),
853 }
854}
855
856async fn read_group(
860 muxer: &mut Muxer,
861 group: &mut moq_net::group::Consumer,
862 broadcast: &moq_net::broadcast::Consumer,
863) -> Result<std::result::Result<Vec<moq_mux::container::Frame>, moq_net::Error>> {
864 match muxer.read(group).await {
865 Ok(frames) => Ok(Ok(frames)),
866 Err(err) => {
867 let miss = net_errors(&err).find(|err| is_unservable(err, broadcast)).cloned();
868 match miss {
869 Some(miss) => Ok(Err(miss)),
870 None => Err(err.into()),
871 }
872 }
873 }
874}
875
876fn is_unservable(err: &moq_net::Error, broadcast: &moq_net::broadcast::Consumer) -> bool {
878 is_cache_miss(err) || is_gone_publisher(err, broadcast)
879}
880
881fn is_cache_miss(err: &moq_net::Error) -> bool {
891 matches!(
892 err,
893 moq_net::Error::NotFound
894 | moq_net::Error::Old
895 | moq_net::Error::Evicted
896 | moq_net::Error::Stream(
897 moq_net::StreamError::NotFound | moq_net::StreamError::Old | moq_net::StreamError::Evicted
898 )
899 )
900}
901
902fn is_gone_publisher(err: &moq_net::Error, broadcast: &moq_net::broadcast::Consumer) -> bool {
909 broadcast.is_closed() || matches!(err, moq_net::Error::Dropped | moq_net::Error::Cancel)
910}
911
912fn net_errors(err: &moq_mux::Error) -> impl Iterator<Item = &moq_net::Error> {
920 std::iter::successors(Some(err as &(dyn std::error::Error + 'static)), |err| err.source())
921 .filter_map(|err| err.downcast_ref::<moq_net::Error>())
922}
923#[cfg(test)]
924mod tests {
925 use super::*;
926
927 fn over_lite(local: &moq_net::Error) -> moq_net::Error {
929 moq_net::Error::from(moq_net::StreamError::from_code(
930 moq_net::StreamError::from(local).to_code(),
931 ))
932 }
933
934 fn over_ietf_miss() -> moq_net::Error {
936 moq_net::Error::from(moq_net::StreamError::from_code(0))
937 }
938
939 fn produce_origin() -> moq_net::origin::Producer {
940 let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default());
941 std::mem::forget(driver);
942 producer
943 }
944
945 #[test]
946 fn a_cache_miss_that_crossed_a_session_is_still_a_cache_miss() {
947 for local in [moq_net::Error::NotFound, moq_net::Error::Old, moq_net::Error::Evicted] {
948 assert!(is_cache_miss(&local), "{local:?} locally");
949 let lite = over_lite(&local);
950 assert!(is_cache_miss(&lite), "{local:?} over lite ({lite:?})");
951 assert!(!is_cache_miss(&over_ietf_miss()), "{local:?} over ietf must not 404");
953 }
954 }
955
956 #[test]
957 fn a_real_failure_is_not_a_cache_miss() {
958 for err in [
959 moq_net::Error::Unauthorized,
960 moq_net::Error::ProtocolViolation,
961 moq_net::Error::Timeout,
962 moq_net::Error::from(moq_net::SessionError::Unauthorized),
963 moq_net::Error::from(moq_net::StreamError::DeliveryTimeout),
964 ] {
965 assert!(!is_cache_miss(&err), "{err:?} locally");
966 assert!(!is_cache_miss(&over_lite(&err)), "{err:?} over lite");
967 }
968 }
969
970 #[test]
971 fn a_cache_miss_is_recognised_through_the_mux_error_layers() {
972 let lite = over_lite(&moq_net::Error::NotFound);
973 assert!(net_errors(&moq_mux::Error::Moq(lite.clone())).any(is_cache_miss));
974 assert!(net_errors(&moq_mux::Error::Hang(hang::Error::Moq(lite.clone()))).any(is_cache_miss));
975 assert!(net_errors(&moq_mux::Error::Cmaf(moq_mux::container::fmp4::Error::Moq(lite))).any(is_cache_miss));
976
977 let denied = moq_net::Error::Unauthorized;
978 assert!(!net_errors(&moq_mux::Error::Moq(denied.clone())).any(is_cache_miss));
979 assert!(!net_errors(&moq_mux::Error::Hang(hang::Error::Moq(denied))).any(is_cache_miss));
980 assert!(!net_errors(&moq_mux::Error::UnknownFormat("nope".to_string())).any(is_cache_miss));
981 }
982
983 #[test]
984 fn a_gone_publisher_is_unservable_and_an_ietf_miss_is_not() {
985 let origin = produce_origin();
986 let producer = origin.create_broadcast("live").expect("publish allowed");
987 let live = producer.consume();
988 let ietf_miss = over_ietf_miss();
989 assert!(
990 !is_gone_publisher(&ietf_miss, &live),
991 "an IETF stream-reset miss on a live publisher must stay 500"
992 );
993 assert!(!is_unservable(&ietf_miss, &live));
994
995 drop(producer);
996 let gone = live;
997 assert!(is_gone_publisher(&ietf_miss, &gone));
998 assert!(is_unservable(&ietf_miss, &gone));
999 assert!(is_gone_publisher(&moq_net::Error::Dropped, &gone));
1000 assert!(is_gone_publisher(&moq_net::Error::Cancel, &gone));
1001 assert!(!is_cache_miss(&moq_net::Error::Dropped));
1002 assert!(!is_cache_miss(&moq_net::Error::Remote(0)));
1003 }
1004}