1use std::collections::VecDeque;
4
5use hang::catalog::VideoConfig;
6
7use super::decoder::Config;
8use super::sink::Sink;
9use crate::Error;
10use crate::Frame;
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum Start {
24 #[default]
30 Oldest,
31 Latest,
37}
38
39#[derive(Clone, Debug, Default)]
48#[non_exhaustive]
49pub struct Options {
50 pub decoder: Config,
52 pub start: Start,
54 pub max_age: std::time::Duration,
60}
61
62impl Options {
63 pub fn new() -> Self {
65 Self::default()
66 }
67}
68
69pub struct Consumer {
76 decoder: Sink,
81 track: moq_mux::container::Consumer<moq_mux::catalog::hang::Container>,
82 pending: VecDeque<Frame>,
86 drained: bool,
88 discontinuity: u64,
90}
91
92impl Consumer {
93 pub async fn new(
96 broadcast: &moq_net::broadcast::Consumer,
97 catalog: &VideoConfig,
98 name: impl Into<String>,
99 options: Options,
100 ) -> Result<Self, Error> {
101 let decoder = Sink::open(catalog, &options.decoder).await?;
102
103 let name = name.into();
104 let track = broadcast.track(&name)?;
105 let mut subscriber = track
106 .subscribe(
107 moq_net::track::Subscription::default()
108 .with_priority(hang::catalog::PRIORITY.video)
109 .with_max_age(options.max_age),
110 )
111 .await?;
112 if options.start == Start::Latest
127 && let Some(live_edge) = track.latest()
128 {
129 subscriber.set_groups(live_edge..);
130 }
131 let track = subscriber;
132 let container = moq_mux::catalog::hang::Container::try_from(catalog)?;
136 let track = moq_mux::container::Consumer::new(track, container);
137
138 Ok(Self {
139 decoder,
140 track,
141 pending: VecDeque::new(),
142 drained: false,
143 discontinuity: 0,
144 })
145 }
146
147 pub fn name(&self) -> &str {
149 self.decoder.name()
150 }
151
152 pub async fn read(&mut self) -> Result<Option<Frame>, Error> {
161 loop {
162 if let Some(frame) = self.pending.pop_front() {
163 return Ok(Some(frame));
164 }
165 if self.drained {
166 return Ok(None);
167 }
168
169 let mux_frame = self.track.read().await?;
170 let discontinuity = self.track.discontinuity();
171 if discontinuity != self.discontinuity {
172 self.discontinuity = discontinuity;
176 }
177
178 let Some(mux_frame) = mux_frame else {
179 let tail = self.decoder.flush().await;
184 self.drained = true;
191 self.pending.extend(tail?);
192 continue;
193 };
194
195 self.pending.extend(
196 self.decoder
197 .decode(mux_frame.payload, mux_frame.timestamp, mux_frame.keyframe)
198 .await?,
199 );
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 #![cfg_attr(not(feature = "openh264"), allow(dead_code, unused_imports))]
207
208 use bytes::Bytes;
209 use moq_net::Timestamp;
210
211 fn produce_origin() -> moq_net::origin::Producer {
213 let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default());
214 if tokio::runtime::Handle::try_current().is_ok() {
215 tokio::spawn(moq_net::time::run(driver));
216 } else {
217 std::mem::forget(driver);
220 }
221 producer
222 }
223 use super::*;
224 use crate::decode::Kind;
225 use crate::decode::backend::probe;
226 use crate::encode::{Config as EncodeConfig, Encoder, Kind as EncodeKind, Producer as EncodeProducer};
227
228 #[tokio::test]
229 #[cfg(feature = "openh264")]
230 async fn reads_cmaf_container_declared_by_catalog() {
231 let mut source_broadcast = moq_net::broadcast::Info::new().produce();
232 let source_subscriber = source_broadcast.consume();
233 let source_catalog =
234 moq_mux::catalog::Producer::new(&mut source_broadcast, moq_mux::catalog::Config::default()).unwrap();
235 let config = EncodeConfig {
236 kind: EncodeKind::Software,
237 ..EncodeConfig::new(320, 240, crate::Rate::new(30, 1).unwrap())
238 };
239 let rendition = config.probe().await.unwrap();
240 let mut producer = EncodeProducer::new(source_broadcast, source_catalog, rendition).unwrap();
241 let mut encoder = Encoder::new(&config).unwrap();
242 let rgba = vec![0x80u8; 320 * 240 * 4];
243 for index in 0..2 {
244 encoder.cut().unwrap();
245 let surface = crate::Surface::rgba(&rgba, crate::Size::new(320, 240)).unwrap();
246 let frame = crate::Frame::new(surface, moq_net::Timestamp::from_micros(index * 33_333).unwrap());
247 producer.publish(&encoder.encode(&frame).unwrap()).unwrap();
248 }
249
250 let origin = produce_origin();
251 let requests = origin.dynamic("", Default::default()).unwrap();
252 let served = source_subscriber.clone();
253 tokio::spawn(async move {
254 while let Ok(request) = requests.requested_broadcast().await {
255 request.accept(served.clone());
256 }
257 });
258 let catalog = moq_mux::catalog::Consumer::<()>::new(&source_subscriber, moq_mux::catalog::CatalogFormat::Hang)
259 .await
260 .unwrap();
261 let source = moq_mux::Source::new(origin.consume(), "test");
262 let mut export =
267 moq_mux::container::fmp4::Export::new(source, catalog).with_max_age(std::time::Duration::from_secs(30));
268 let init = export.next().await.unwrap().expect("CMAF init");
269 let fragment = export.next().await.unwrap().expect("CMAF fragment");
270
271 let mut broadcast = moq_net::broadcast::Info::new().produce();
272 let subscriber = broadcast.consume();
273 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
274 let mut import = moq_mux::container::fmp4::Import::new(broadcast, catalog.reserve());
275 import.decode(&init).unwrap();
276 import.decode(&fragment).unwrap();
277
278 let snapshot = catalog.snapshot();
279 let (name, config) = snapshot.video.renditions.iter().next().expect("video rendition");
280 assert!(matches!(config.container, hang::catalog::Container::Cmaf { .. }));
281 let mut consumer = Consumer::new(
282 &subscriber,
283 config,
284 name,
285 Options {
286 decoder: Config {
287 kind: Kind::Software,
288 ..Config::new()
289 },
290 ..Options::new()
291 },
292 )
293 .await
294 .unwrap();
295
296 let frame = consumer.read().await.unwrap().expect("decoded frame");
297 assert_eq!(frame.size(), crate::Size::new(320, 240));
298 }
299
300 #[tokio::test]
308 async fn a_second_consumer_starts_at_the_live_edge() {
309 let broadcast = moq_net::broadcast::Info::new().produce();
310 let track = broadcast
311 .create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video))
312 .unwrap();
313 let published = track.clone();
315 let subscriber = broadcast.consume();
316 let mut producer = moq_mux::container::Producer::new(
317 track,
318 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
319 );
320 for index in 0..3u64 {
322 producer
323 .write(moq_mux::container::Frame {
324 timestamp: Timestamp::from_micros(index * 1_000_000).unwrap(),
325 duration: None,
326 payload: Bytes::from_static(b"access unit"),
327 keyframe: true,
328 })
329 .unwrap();
330 }
331 producer.finish().unwrap();
332
333 let catalog = VideoConfig::new(hang::catalog::H264 {
334 inline: true,
335 profile: 0x42,
336 constraints: 0,
337 level: 30,
338 });
339 let mut consumer = Consumer::new(
340 &subscriber,
341 &catalog,
342 "video",
343 Options {
344 decoder: Config {
345 kind: Kind::Named(probe::BUFFERED_NAME.into()),
346 ..Config::new()
347 },
348 start: Start::Latest,
349 max_age: std::time::Duration::from_secs(10),
353 ..Options::new()
354 },
355 )
356 .await
357 .unwrap();
358
359 let frame = consumer.read().await.unwrap().expect("a decoded frame");
364 assert_eq!(
365 frame.timestamp,
366 Timestamp::from_micros(2_000_000).unwrap(),
367 "a fresh consumer replayed the groups an earlier reader still holds",
368 );
369
370 assert_eq!(
378 published.subscription().and_then(|sub| sub.start),
379 None,
380 "the publisher was asked to rewind the track",
381 );
382 }
383
384 #[tokio::test]
392 async fn the_default_reads_every_cached_group() {
393 let broadcast = moq_net::broadcast::Info::new().produce();
394 let track = broadcast
395 .create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video))
396 .unwrap();
397 let subscriber = broadcast.consume();
398 let mut producer = moq_mux::container::Producer::new(
399 track,
400 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
401 );
402 for index in 0..3u64 {
403 producer
404 .write(moq_mux::container::Frame {
405 timestamp: Timestamp::from_micros(index * 1_000_000).unwrap(),
406 duration: None,
407 payload: Bytes::from_static(b"access unit"),
408 keyframe: true,
409 })
410 .unwrap();
411 }
412 producer.finish().unwrap();
413
414 let catalog = VideoConfig::new(hang::catalog::H264 {
415 inline: true,
416 profile: 0x42,
417 constraints: 0,
418 level: 30,
419 });
420 let mut consumer = Consumer::new(
421 &subscriber,
422 &catalog,
423 "video",
424 Options {
425 decoder: Config {
426 kind: Kind::Named(probe::BUFFERED_NAME.into()),
429 ..Config::new()
430 },
431 max_age: std::time::Duration::from_secs(10),
434 ..Options::new()
435 },
436 )
437 .await
438 .unwrap();
439
440 let mut seen = Vec::new();
441 while let Some(frame) = consumer.read().await.unwrap() {
442 seen.push(frame.timestamp);
443 }
444 assert_eq!(
445 seen,
446 vec![
447 Timestamp::from_micros(0).unwrap(),
448 Timestamp::from_micros(1_000_000).unwrap(),
449 Timestamp::from_micros(2_000_000).unwrap(),
450 ],
451 "the default dropped groups the caller never asked to drop",
452 );
453 }
454
455 #[test]
462 fn max_age_reaches_the_subscription_and_not_the_decoder() {
463 let _probe = probe::native_exclusive();
464 let broadcast = moq_net::broadcast::Info::new().produce();
465 let track = broadcast
466 .create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video))
467 .unwrap();
468 let published = track.clone();
469 let subscriber = broadcast.consume();
470 let mut producer = moq_mux::container::Producer::new(
471 track,
472 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
473 );
474 producer
475 .write(moq_mux::container::Frame {
476 timestamp: Timestamp::from_micros(0).unwrap(),
477 duration: None,
478 payload: Bytes::from_static(b"access unit"),
479 keyframe: true,
480 })
481 .unwrap();
482 producer.finish().unwrap();
483
484 let catalog = VideoConfig::new(hang::catalog::H264 {
485 inline: true,
486 profile: 0x42,
487 constraints: 0,
488 level: 30,
489 });
490 let decoder = Config {
491 kind: Kind::Named(probe::NATIVE_NAME.into()),
492 output: crate::Output::Cpu,
493 scale_hint: Some(crate::Size::new(160, 120)),
494 };
495 let max_age = std::time::Duration::from_secs(10);
496 let mut consumer = pollster::block_on(Consumer::new(
497 &subscriber,
498 &catalog,
499 "video",
500 Options {
501 decoder: decoder.clone(),
502 max_age,
503 ..Options::new()
504 },
505 ))
506 .unwrap();
507
508 let subscription = published.subscription().expect("the consumer subscribed");
509 assert_eq!(
510 subscription.max_age, max_age,
511 "the age budget did not reach the publisher"
512 );
513
514 let opened = probe::native_opened().expect("the decoder opened");
515 assert_eq!(opened.output, decoder.output);
516 assert_eq!(opened.scale_hint, decoder.scale_hint);
517
518 let frame = pollster::block_on(consumer.read()).unwrap().expect("a decoded frame");
519 assert!(
520 matches!(frame.surface, crate::Surface::I420(_)),
521 "CPU output was not enforced"
522 );
523 }
524
525 #[tokio::test]
528 async fn track_end_drains_buffered_decoder() {
529 let broadcast = moq_net::broadcast::Info::new().produce();
530 let track = broadcast
531 .create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video))
532 .unwrap();
533 let subscriber = broadcast.consume();
534 let mut producer = moq_mux::container::Producer::new(
535 track,
536 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
537 );
538 for index in 0..2u64 {
539 producer
540 .write(moq_mux::container::Frame {
541 timestamp: Timestamp::from_micros(index * 33_333).unwrap(),
542 duration: None,
543 payload: Bytes::from_static(b"access unit"),
544 keyframe: index == 0,
545 })
546 .unwrap();
547 }
548 producer.finish().unwrap();
549
550 let catalog = VideoConfig::new(hang::catalog::H264 {
551 inline: true,
552 profile: 0x42,
553 constraints: 0,
554 level: 30,
555 });
556 let mut consumer = Consumer::new(
557 &subscriber,
558 &catalog,
559 "video",
560 Options {
561 decoder: Config {
562 kind: Kind::Named(probe::BUFFERED_NAME.into()),
563 ..Config::new()
564 },
565 ..Options::new()
566 },
567 )
568 .await
569 .unwrap();
570
571 let mut timestamps = Vec::new();
572 while let Some(frame) = consumer.read().await.unwrap() {
573 timestamps.push(frame.timestamp.as_micros());
574 }
575 assert_eq!(timestamps, vec![0, 33_333]);
576 assert!(
577 consumer.read().await.unwrap().is_none(),
578 "the decoder was drained twice"
579 );
580 }
581
582 #[tokio::test]
585 async fn discontinuity_does_not_flush_the_decoder() {
586 let broadcast = moq_net::broadcast::Info::new().produce();
587 let track = broadcast
588 .create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video))
589 .unwrap();
590 let subscriber = broadcast.consume();
591 let mut producer = moq_mux::container::Producer::new(
592 track,
593 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Video),
594 );
595 producer
596 .write(moq_mux::container::Frame {
597 timestamp: Timestamp::from_micros(100_000).unwrap(),
598 duration: None,
599 payload: Bytes::from_static(b"old access unit"),
600 keyframe: true,
601 })
602 .unwrap();
603 producer.discontinuity().unwrap();
604 producer
605 .write(moq_mux::container::Frame {
606 timestamp: Timestamp::from_micros(200_000).unwrap(),
607 duration: None,
608 payload: Bytes::from_static(b"new access unit"),
609 keyframe: true,
610 })
611 .unwrap();
612 producer.finish().unwrap();
613
614 let catalog = VideoConfig::new(hang::catalog::H264 {
615 inline: true,
616 profile: 0x42,
617 constraints: 0,
618 level: 30,
619 });
620 let mut consumer = Consumer::new(
621 &subscriber,
622 &catalog,
623 "video",
624 Options {
625 decoder: Config {
626 kind: Kind::Named(probe::BUFFERED_NAME.into()),
627 ..Config::new()
628 },
629 max_age: std::time::Duration::from_secs(10),
630 ..Options::new()
631 },
632 )
633 .await
634 .unwrap();
635
636 let mut timestamps = Vec::new();
637 while let Some(frame) = consumer.read().await.unwrap() {
638 timestamps.push(frame.timestamp.as_micros());
639 }
640 assert_eq!(timestamps, vec![100_000, 200_000]);
641 }
642
643 #[cfg(not(target_os = "macos"))]
647 #[tokio::test]
648 async fn cancelled_track_end_flush_is_not_reported_as_drained() {
649 probe::prepare_blocking_flush();
650 let broadcast = moq_net::broadcast::Info::new().produce();
651 let track = broadcast
652 .create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video))
653 .unwrap();
654 let subscriber = broadcast.consume();
655 let mut producer = moq_mux::container::Producer::new(
656 track,
657 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
658 );
659 producer.finish().unwrap();
660
661 let catalog = VideoConfig::new(hang::catalog::H264 {
662 inline: true,
663 profile: 0x42,
664 constraints: 0,
665 level: 30,
666 });
667 let mut consumer = Consumer::new(
668 &subscriber,
669 &catalog,
670 "video",
671 Options {
672 decoder: Config {
673 kind: Kind::Named(probe::BLOCKING_FLUSH_NAME.into()),
674 ..Config::new()
675 },
676 ..Options::new()
677 },
678 )
679 .await
680 .unwrap();
681
682 let mut read = Box::pin(consumer.read());
683 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1);
684 loop {
685 tokio::select! {
686 _result = &mut read => panic!("flush returned before cancellation"),
687 _ = tokio::time::sleep(std::time::Duration::from_millis(1)) => {
688 if probe::flush_entered() {
689 break;
690 }
691 if tokio::time::Instant::now() >= deadline {
692 probe::release_flush();
693 panic!("flush never reached the codec thread");
694 }
695 }
696 }
697 }
698 drop(read);
699 probe::release_flush();
700
701 let err = match consumer.read().await {
702 Err(err) => err,
703 Ok(_) => panic!("cancelled flush must poison the sink"),
704 };
705 assert!(err.to_string().contains("cancelled call"), "unexpected error: {err}");
706 assert!(matches!(err, crate::Error::CodecGone(_)));
707 assert!(consumer.read().await.unwrap().is_none());
708 }
709
710 #[cfg(all(target_os = "linux", feature = "vaapi"))]
712 #[tokio::test]
713 async fn the_track_ending_drains_the_decoder() {
714 const FRAMES: u64 = 5;
715 let config = EncodeConfig {
716 kind: EncodeKind::Software,
717 ..EncodeConfig::new(320, 240, crate::Rate::new(30, 1).unwrap())
718 };
719 let catalog = config.probe().await.expect("probe the software encoder");
720
721 let broadcast = moq_net::broadcast::Info::new().produce();
722 let track = broadcast
723 .create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video))
724 .unwrap();
725 let subscriber = broadcast.consume();
726 let mut producer = moq_mux::container::Producer::new(
727 track,
728 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Data),
729 );
730
731 let mut encoder = Encoder::new(&config).unwrap();
732 let rgba = vec![0x80u8; 320 * 240 * 4];
733 for index in 0..FRAMES {
734 if index == 0 {
735 encoder.cut().unwrap();
736 }
737 let surface = crate::Surface::rgba(&rgba, crate::Size::new(320, 240)).unwrap();
738 let frame = crate::Frame::new(surface, moq_net::Timestamp::from_micros(index * 33_333).unwrap());
739 for encoded in encoder.encode(&frame).unwrap() {
740 producer
741 .write(moq_mux::container::Frame {
742 timestamp: encoded.timestamp,
743 duration: None,
744 payload: encoded.payload,
745 keyframe: index == 0,
746 })
747 .unwrap();
748 }
749 }
750 producer.finish().unwrap();
751
752 let decode = Options {
753 decoder: Config {
754 kind: Kind::Named("vaapi".into()),
755 ..Config::new()
756 },
757 ..Options::new()
758 };
759 let Ok(mut consumer) = Consumer::new(&subscriber, &catalog, "video", decode).await else {
762 return;
763 };
764
765 let mut timestamps = Vec::new();
766 while let Some(frame) = consumer.read().await.unwrap() {
767 timestamps.push(frame.timestamp.as_micros());
768 }
769 let expected: Vec<u128> = (0..FRAMES as u128).map(|index| index * 33_333).collect();
770 assert_eq!(timestamps, expected, "the track ended before the stream did");
771
772 assert!(consumer.read().await.unwrap().is_none());
775 }
776}