1use std::collections::VecDeque;
4
5use bytes::Bytes;
6
7use super::decoder::{Config, Decoder, Start};
8use crate::resample::{Resampler, remix, validate_channels};
9use crate::{Activity, Error, Frame};
10
11pub struct Consumer {
19 decoder: Decoder,
20 track: moq_mux::container::Consumer<moq_mux::catalog::hang::Container>,
21 resampler: Option<Resampler>,
22 config: Config,
23 latency_max: std::time::Duration,
24 resolved_sample_rate: u32,
25 resolved_channels: u32,
26 next_start: Option<moq_net::Timestamp>,
30 ready: VecDeque<Frame>,
33 spans: VecDeque<ActivitySpan>,
35 trailing: Activity,
38 epoch: Option<moq_net::Timestamp>,
41 delay_trimmed: usize,
43 frames_decoded: usize,
45 end: Option<moq_net::Timestamp>,
47 terminal_start: Option<moq_net::Timestamp>,
49 discontinuity: u64,
51}
52
53struct ActivitySpan {
54 end: moq_net::Timestamp,
55 activity: Activity,
56}
57
58impl Consumer {
59 pub async fn new(
62 broadcast: &moq_net::broadcast::Consumer,
63 catalog: &hang::catalog::AudioConfig,
64 name: impl Into<String>,
65 config: Config,
66 ) -> Result<Self, Error> {
67 let decoder = Decoder::new(catalog)?;
68 let sample_rate = config.sample_rate.unwrap_or_else(|| decoder.sample_rate());
69 let channels = config.channels.unwrap_or_else(|| decoder.channel_count());
70 validate_channels(channels)?;
71
72 let resampler = if sample_rate == decoder.sample_rate() {
73 None
74 } else {
75 let chunk_frames = (decoder.sample_rate() as usize * 20) / 1000;
76 Some(Resampler::new(
77 decoder.sample_rate(),
78 sample_rate,
79 decoder.channel_count(),
80 chunk_frames,
81 )?)
82 };
83
84 let name = name.into();
85 let track = broadcast.track(&name)?;
86 let mut subscriber = track
87 .subscribe(moq_net::track::Subscription::default().with_priority(hang::catalog::PRIORITY.audio))
88 .await?;
89 if config.start == Start::Latest
104 && let Some(live_edge) = track.latest()
105 {
106 subscriber.start_at(live_edge);
107 }
108 let track = subscriber;
109 let latency_max = config.latency_max.unwrap_or_default().min(track.info().latency_max);
110 let container = moq_mux::catalog::hang::Container::try_from(&catalog.container)?;
114 let mut track = moq_mux::container::Consumer::new(track, container);
115 if let Some(latency) = config.latency_max {
116 track = track.with_latency(latency);
117 }
118
119 Ok(Self {
120 decoder,
121 track,
122 resampler,
123 config,
124 latency_max,
125 resolved_sample_rate: sample_rate,
126 resolved_channels: channels,
127 next_start: None,
128 ready: VecDeque::new(),
129 spans: VecDeque::new(),
130 trailing: Activity::Active,
131 epoch: None,
132 delay_trimmed: 0,
133 frames_decoded: 0,
134 end: None,
135 terminal_start: None,
136 discontinuity: 0,
137 })
138 }
139
140 pub fn config(&self) -> &Config {
142 &self.config
143 }
144
145 pub fn latency_max(&self) -> std::time::Duration {
147 self.latency_max
148 }
149
150 pub fn sample_rate(&self) -> u32 {
153 self.resolved_sample_rate
154 }
155
156 pub fn channels(&self) -> u32 {
159 self.resolved_channels
160 }
161
162 pub async fn read(&mut self) -> Result<Option<Frame>, Error> {
175 loop {
176 if let Some(frame) = self.ready.pop_front() {
177 return Ok(Some(frame));
178 }
179
180 let mux_frame = self.track.read().await?;
181 self.apply_discontinuity()?;
182 let Some(mux_frame) = mux_frame else {
183 return self.flush();
184 };
185
186 if let Some(end) = self.track.end()
187 && self.end != Some(end)
188 {
189 self.end = Some(end);
190 self.frames_decoded = 0;
191 self.terminal_start = None;
192 }
193
194 if self.end.is_none()
202 && self
203 .next_start
204 .is_some_and(|next| discontinuous(next, mux_frame.timestamp))
205 && let Some(frame) = self.gap()?
206 {
207 self.ready.push_back(frame);
208 }
209
210 let rate = self.decoder.sample_rate();
211 let epoch = *self.epoch.get_or_insert(mux_frame.timestamp);
212 let delay = self.decoder.delay_remaining();
213 let decoded = self.decoder.decode(&mux_frame.payload)?;
214 let trimmed = delay - self.decoder.delay_remaining();
217 self.delay_trimmed += trimmed;
218 let activity = decoded.activity;
219 let mut decoded = decoded.samples;
220 if let Some(end) = self.end {
221 let terminal_start = *self
222 .terminal_start
223 .get_or_insert(rewind(mux_frame.timestamp, self.delay_trimmed, rate)?.max(epoch));
224 let total = frames_between(terminal_start, end, rate)?;
225 let remaining = total.saturating_sub(self.frames_decoded);
226 decoded.truncate(remaining.saturating_mul(self.decoder.channel_count() as usize));
227 }
228
229 let frames = decoded.len() / self.decoder.channel_count().max(1) as usize;
230 let decoded_at = if let Some(terminal_start) = self.terminal_start {
231 advance(terminal_start, self.frames_decoded, rate)?
232 } else {
233 rewind(mux_frame.timestamp, self.delay_trimmed, rate)?.max(epoch)
237 };
238 if self.end.is_some() {
239 self.frames_decoded += frames;
240 }
241 self.next_start = Some(advance(mux_frame.timestamp, frames + trimmed, rate)?);
244 if decoded.is_empty() {
245 continue;
246 }
247
248 let (pcm, timestamp) = match self.resampler.as_mut() {
249 Some(r) => {
256 let held = if r.pending_frames() == 0 {
257 decoded_at
258 } else {
259 r.held_at().unwrap_or(decoded_at)
260 };
261 let skipped = r.skipped();
262 let pcm = r.process(&decoded, decoded_at)?;
263 (pcm, rewind(held, skipped, self.resolved_sample_rate)?)
264 }
265 None => (decoded, decoded_at),
266 };
267
268 let decoded_end = advance(decoded_at, frames, rate)?;
269
270 let resampled = self.resampler.is_some();
275 if resampled {
276 self.spans.push_back(ActivitySpan {
277 end: decoded_end,
278 activity,
279 });
280 }
281
282 if pcm.is_empty() {
286 continue;
287 }
288
289 let activity = if resampled {
290 self.activity_at(timestamp)
291 } else {
292 activity
293 };
294 let frame = self.frame(pcm, timestamp, activity)?;
297 self.ready.push_back(frame);
298 }
299 }
300
301 fn apply_discontinuity(&mut self) -> Result<(), Error> {
303 let discontinuity = self.track.discontinuity();
304 if discontinuity == self.discontinuity {
305 return Ok(());
306 }
307
308 self.discontinuity = discontinuity;
309 self.decoder.reset()?;
310 if let Some(resampler) = self.resampler.as_mut() {
311 resampler.reset();
312 }
313 self.next_start = None;
314 self.spans.clear();
315 self.trailing = Activity::Active;
316 self.epoch = None;
317 self.delay_trimmed = 0;
318 self.frames_decoded = 0;
319 self.end = None;
320 self.terminal_start = None;
321 Ok(())
322 }
323
324 fn gap(&mut self) -> Result<Option<Frame>, Error> {
333 self.decoder.reset_prediction()?;
334
335 let mut frame = None;
336 if let Some(resampler) = self.resampler.as_mut() {
337 let held = resampler.held_at();
338 let skipped = resampler.skipped();
339 let pcm = resampler.drain()?;
340 frame = self.tail(pcm, held, skipped)?;
341 }
342
343 self.next_start = None;
344 self.spans.clear();
345 self.trailing = Activity::Active;
346 self.epoch = None;
347 self.delay_trimmed = 0;
348 Ok(frame)
349 }
350
351 fn flush(&mut self) -> Result<Option<Frame>, Error> {
358 let Some(resampler) = self.resampler.take() else {
359 return Ok(None);
360 };
361
362 let held = resampler.held_at();
363 let skipped = resampler.skipped();
364 self.tail(resampler.flush()?, held, skipped)
365 }
366
367 fn tail(
373 &mut self,
374 pcm: Vec<f32>,
375 held: Option<moq_net::Timestamp>,
376 skipped: usize,
377 ) -> Result<Option<Frame>, Error> {
378 let Some(held) = held.filter(|_| !pcm.is_empty()) else {
379 return Ok(None);
380 };
381
382 let timestamp = rewind(held, skipped, self.resolved_sample_rate)?;
383 let activity = self.activity_at(timestamp);
384 Ok(Some(self.frame(pcm, timestamp, activity)?))
385 }
386
387 fn activity_at(&mut self, timestamp: moq_net::Timestamp) -> Activity {
389 while let Some(span) = self.spans.front().filter(|span| span.end <= timestamp) {
390 self.trailing = span.activity;
391 self.spans.pop_front();
392 }
393
394 self.spans.front().map_or(self.trailing, |span| span.activity)
395 }
396
397 fn frame(&self, pcm: Vec<f32>, timestamp: moq_net::Timestamp, activity: Activity) -> Result<Frame, Error> {
399 let pcm = if self.decoder.channel_count() == self.resolved_channels {
400 pcm
401 } else {
402 remix(&pcm, self.decoder.channel_count(), self.resolved_channels)?
403 };
404
405 let bytes = self.config.format.from_interleaved_f32(&pcm, self.resolved_channels)?;
406 Ok(Frame {
407 timestamp,
408 data: Bytes::from(bytes),
409 activity,
410 })
411 }
412}
413
414fn discontinuous(expected: moq_net::Timestamp, timestamp: moq_net::Timestamp) -> bool {
438 let scale = expected.scale().max(timestamp.scale());
439 let quantum = scale.min(moq_net::Timescale::default());
440 let tolerance = (scale.as_u64() as u128).div_ceil(quantum.as_u64() as u128) + 1;
441 expected.as_scale(scale).abs_diff(timestamp.as_scale(scale)) > tolerance
442}
443
444fn advance(timestamp: moq_net::Timestamp, frames: usize, sample_rate: u32) -> Result<moq_net::Timestamp, Error> {
446 if frames == 0 {
447 return Ok(timestamp);
448 }
449
450 let offset = moq_net::Timestamp::from_scale(frames as u64, sample_rate as u64)?.convert(timestamp.scale())?;
451 Ok(timestamp.checked_add(offset)?)
452}
453
454fn frames_between(start: moq_net::Timestamp, end: moq_net::Timestamp, sample_rate: u32) -> Result<usize, Error> {
456 let duration = end.checked_sub(start)?;
457 let frames = (std::time::Duration::from(duration).as_nanos() * sample_rate as u128 + 500_000_000) / 1_000_000_000;
458 usize::try_from(frames).map_err(|_| Error::Unsupported("audio duration does not fit in memory".into()))
459}
460
461fn rewind(timestamp: moq_net::Timestamp, frames: usize, sample_rate: u32) -> Result<moq_net::Timestamp, Error> {
466 if frames == 0 {
467 return Ok(timestamp);
468 }
469
470 let offset = moq_net::Timestamp::from_scale(frames as u64, sample_rate as u64)?.convert(timestamp.scale())?;
471 Ok(timestamp
472 .checked_sub(offset)
473 .unwrap_or(moq_net::Timestamp::new(0, timestamp.scale())?))
474}
475
476#[cfg(test)]
477mod tests {
478 use moq_net::Timestamp;
479
480 use super::*;
481 use crate::Format;
482 use crate::encode::{Encoder, Input, Options, Producer};
483
484 #[tokio::test]
485 async fn remixes_mono_stream_to_stereo_output() {
486 let mut broadcast = moq_net::broadcast::Info::new().produce();
487 let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
488 let subscriber = broadcast.consume();
489 let input = Input {
490 format: Format::F32,
491 sample_rate: 48_000,
492 channels: 1,
493 };
494 let options = Options {
495 track: Some("audio".to_string()),
496 ..Options::default()
497 };
498 let mut producer = Producer::new(&mut broadcast, catalog, input.clone(), &options).unwrap();
499 let catalog = Encoder::new(&crate::encode::Config::new(input)).unwrap().catalog();
500 let mut consumer = Consumer::new(
501 &subscriber,
502 &catalog,
503 "audio",
504 Config {
505 channels: Some(2),
506 ..Config::new()
507 },
508 )
509 .await
510 .unwrap();
511
512 let samples = vec![0.1f32; 960];
513 let mut data = Vec::with_capacity(samples.len() * size_of::<f32>());
514 for sample in samples {
515 data.extend_from_slice(&sample.to_le_bytes());
516 }
517 producer.write(&Frame::new(data.into(), Timestamp::ZERO)).unwrap();
518
519 let frame = consumer.read().await.unwrap().expect("decoded frame");
520 let samples = Format::F32.as_interleaved_f32(&frame.data, 2).unwrap();
521 assert_eq!(samples.len(), (960 - 312) * 2);
522 for pair in samples.chunks_exact(2) {
523 assert_eq!(pair[0], pair[1]);
524 }
525 }
526
527 #[tokio::test]
534 async fn resampled_timestamps_follow_the_samples() {
535 let mut broadcast = moq_net::broadcast::Info::new().produce();
536 let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
537 let subscriber = broadcast.consume();
538
539 let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 44_100, 1);
540 let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
541
542 let mut consumer = Consumer::new(
543 &subscriber,
544 &catalog,
545 "audio",
546 Config {
547 sample_rate: Some(48_000),
548 ..Config::new()
549 },
550 )
551 .await
552 .unwrap();
553
554 const FRAMES: u64 = 1024;
556 let payload: Bytes = vec![0u8; FRAMES as usize * size_of::<f32>()].into();
557 for packet in 0..2 {
558 producer
559 .write(moq_mux::container::Frame {
560 timestamp: moq_net::Timestamp::from_scale(packet * FRAMES, 44_100).unwrap(),
561 duration: None,
562 payload: payload.clone(),
563 keyframe: true,
564 })
565 .unwrap();
566 }
567
568 let first = consumer.read().await.unwrap().expect("decoded frame");
569 assert_eq!(first.timestamp.as_micros(), 0);
570
571 let second = consumer.read().await.unwrap().expect("decoded frame");
577 let first_frames = (first.data.len() / size_of::<f32>()) as u128;
578 let ends_at = first_frames * 1_000_000 / 48_000;
579 let gap = second.timestamp.as_micros().abs_diff(ends_at);
580 assert!(gap < 100, "expected the frames to meet, got a {gap} us gap");
581 }
582
583 #[tokio::test]
587 async fn resampled_tail_survives_the_end_of_the_track() {
588 let mut broadcast = moq_net::broadcast::Info::new().produce();
589 let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
590 let subscriber = broadcast.consume();
591
592 let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 44_100, 1);
593 let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
594
595 let mut consumer = Consumer::new(
596 &subscriber,
597 &catalog,
598 "audio",
599 Config {
600 sample_rate: Some(48_000),
601 ..Config::new()
602 },
603 )
604 .await
605 .unwrap();
606
607 const FRAMES: usize = 1024;
609 let payload: Bytes = vec![0u8; FRAMES * size_of::<f32>()].into();
610 producer
611 .write(moq_mux::container::Frame {
612 timestamp: moq_net::Timestamp::ZERO,
613 duration: None,
614 payload,
615 keyframe: true,
616 })
617 .unwrap();
618 producer.finish().unwrap();
619
620 let first = consumer.read().await.unwrap().expect("decoded frame");
621 let first_frames = first.data.len() / size_of::<f32>();
622
623 let tail = consumer.read().await.unwrap().expect("flushed tail");
624 let tail_frames = tail.data.len() / size_of::<f32>();
625
626 assert!((215..=230).contains(&tail_frames), "unexpected tail: {tail_frames}");
630 let ends_at = (first_frames as u128) * 1_000_000 / 48_000;
633 let gap = tail.timestamp.as_micros().abs_diff(ends_at);
634 assert!(gap < 100, "expected the tail to meet the body, got a {gap} us gap");
635
636 let total = first_frames + tail_frames;
640 assert!((1105..=1120).contains(&total), "unexpected total: {total}");
641 assert!(consumer.read().await.unwrap().is_none());
642 }
643
644 #[tokio::test]
645 async fn resampling_keeps_the_activity_boundary_on_its_source() {
646 let mut encoder = Encoder::new(&crate::encode::Config {
647 dtx: true,
648 bitrate: Some(24_000),
649 frame_duration: std::time::Duration::from_millis(10),
650 ..crate::encode::Config::new(Input {
651 channels: 1,
652 ..Input::default()
653 })
654 })
655 .unwrap();
656 let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 1);
657
658 let mut broadcast = moq_net::broadcast::Info::new().produce();
659 let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
660 let subscriber = broadcast.consume();
661 let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
662 let mut consumer = Consumer::new(
663 &subscriber,
664 &catalog,
665 "audio",
666 Config {
667 sample_rate: Some(44_100),
668 ..Config::new()
669 },
670 )
671 .await
672 .unwrap();
673
674 let active = vec![0.5; encoder.frame_size()];
675 let silence = vec![0.0; encoder.frame_size()];
676 let mut first_dtx = None;
677 for index in 0..40u64 {
678 let packet = encoder.encode(if index == 0 { &active } else { &silence }).unwrap();
679 let timestamp = Timestamp::from_scale(index * encoder.frame_size() as u64, 48_000).unwrap();
680 if first_dtx.is_none() && packet.activity.is_dtx() {
681 first_dtx = Some(timestamp);
682 }
683 producer
684 .write(moq_mux::container::Frame {
685 timestamp,
686 payload: packet.payload,
687 keyframe: true,
688 duration: None,
689 })
690 .unwrap();
691 producer.cut(None).unwrap();
692 }
693 producer.finish().unwrap();
694
695 let expected = first_dtx.expect("silence should enter Opus DTX");
696 let mut actual = None;
697 while let Some(frame) = consumer.read().await.unwrap() {
698 assert!(!frame.data.is_empty(), "read returned a frame with no samples");
703 if frame.activity.is_dtx() {
704 actual = Some(frame.timestamp);
705 break;
706 }
707 }
708 let actual = actual.expect("consumer should report Opus DTX");
709
710 let delay = actual.as_micros() as i128 - expected.as_micros() as i128;
717 let chunk_us = 20_000i128;
718 assert!(
719 (0..chunk_us).contains(&delay),
720 "DTX label landed {delay} us from its source, outside [0, {chunk_us})"
721 );
722 }
723
724 async fn pcm_gaps(rate: u32, out_rate: u32, frames: usize, stamps: &[Timestamp]) -> Vec<(u128, usize)> {
727 let mut broadcast = moq_net::broadcast::Info::new().produce();
728 let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
729 let subscriber = broadcast.consume();
730
731 let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, rate, 1);
732 let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
733 let mut consumer = Consumer::new(
734 &subscriber,
735 &catalog,
736 "audio",
737 Config {
738 sample_rate: Some(out_rate),
739 ..Config::new()
740 },
741 )
742 .await
743 .unwrap();
744
745 let payload: Bytes = vec![0u8; frames * size_of::<f32>()].into();
746 for stamp in stamps {
747 producer
748 .write(moq_mux::container::Frame {
749 timestamp: *stamp,
750 duration: None,
751 payload: payload.clone(),
752 keyframe: true,
753 })
754 .unwrap();
755 }
756 producer.finish().unwrap();
757
758 let mut read = Vec::new();
759 while let Some(frame) = consumer.read().await.unwrap() {
760 read.push((frame.timestamp.as_micros(), frame.data.len() / size_of::<f32>()));
761 }
762 read
763 }
764
765 #[tokio::test]
770 async fn a_missing_packet_leaves_a_hole() {
771 const FRAMES: usize = 1024;
772 let stamps = [
774 Timestamp::from_scale(0, 44_100).unwrap(),
775 Timestamp::from_scale(2 * FRAMES as u64, 44_100).unwrap(),
776 ];
777 let read = pcm_gaps(44_100, 48_000, FRAMES, &stamps).await;
778
779 assert_eq!(read.len(), 4, "unexpected frames: {read:?}");
782
783 let before: usize = read[..2].iter().map(|(_, frames)| frames).sum();
786 assert!((1105..=1120).contains(&before), "unexpected pre-gap audio: {before}");
787
788 assert_eq!(read[2].0, stamps[1].as_micros());
792
793 let ends_at = read[1].0 + (read[1].1 as u128) * 1_000_000 / 48_000;
795 let hole = read[2].0 - ends_at;
796 assert!((23_100..=23_350).contains(&hole), "unexpected hole: {hole} us");
797 }
798
799 #[tokio::test]
805 async fn a_jump_inside_the_slack_leaves_the_held_samples_alone() {
806 const FRAMES: usize = 441;
809 let stamps = [
812 Timestamp::from_micros(0).unwrap(),
813 Timestamp::from_micros(11_000).unwrap(),
814 ];
815 let read = pcm_gaps(44_100, 48_000, FRAMES, &stamps).await;
816
817 assert_eq!(read.len(), 2, "unexpected frames: {read:?}");
820 assert_eq!(read[0].0, 0, "held samples moved with the jump: {read:?}");
823 }
824
825 #[tokio::test]
826 async fn a_jump_after_a_full_chunk_uses_the_new_packet_timestamp() {
827 let stamps = [
828 Timestamp::from_micros(0).unwrap(),
829 Timestamp::from_micros(21_000).unwrap(),
830 ];
831 let read = pcm_gaps(44_100, 48_000, 882, &stamps).await;
832 let mut r = crate::Resampler::new(44_100, 48_000, 1, 882).unwrap();
833 r.process(&[0.25; 882], stamps[0]).unwrap();
834 let expected = rewind(stamps[1], r.skipped(), 48_000).unwrap().as_micros();
835 assert_eq!(read[1].0, expected);
836 }
837
838 #[tokio::test]
844 async fn a_terminal_jump_leaves_the_held_samples_alone() {
845 let mut encoder = Encoder::new(&crate::encode::Config {
846 dtx: true,
847 bitrate: Some(24_000),
848 ..crate::encode::Config::new(Input {
849 channels: 1,
850 ..Input::default()
851 })
852 })
853 .unwrap();
854 let catalog = encoder.catalog();
855
856 let active = encoder.encode(&vec![0.5f32; encoder.frame_size()]).unwrap();
860 assert!(active.activity.is_active());
861 let silence = vec![0.0f32; encoder.frame_size()];
862 let dtx = (0..200)
863 .map(|_| encoder.encode(&silence).unwrap())
864 .find(|packet| packet.activity.is_dtx())
865 .expect("silence should enter Opus DTX");
866
867 let mut broadcast = moq_net::broadcast::Info::new().produce();
868 let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
869 let subscriber = broadcast.consume();
870 let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
871 let mut consumer = Consumer::new(
872 &subscriber,
873 &catalog,
874 "audio",
875 Config {
876 sample_rate: Some(44_100),
877 ..Config::new()
878 },
879 )
880 .await
881 .unwrap();
882
883 let write = |producer: &mut moq_mux::container::Producer<_>, frames: u64, payload: Bytes| {
886 producer
887 .write(moq_mux::container::Frame {
888 timestamp: Timestamp::from_scale(frames, 48_000).unwrap(),
889 duration: None,
890 payload,
891 keyframe: true,
892 })
893 .unwrap();
894 };
895 write(&mut producer, 0, active.payload);
896 write(&mut producer, 3 * 48_000, Bytes::new());
898 write(&mut producer, 48_000, dtx.payload);
899 producer.finish().unwrap();
900
901 let frame = consumer.read().await.unwrap().expect("decoded frame");
902 assert_eq!(frame.timestamp.as_micros(), 0, "held samples moved with the jump");
906 assert!(
907 frame.activity.is_active(),
908 "held samples took the terminal packet's label"
909 );
910 }
911
912 #[tokio::test]
920 async fn millisecond_stamps_are_not_a_gap() {
921 const FRAMES: u64 = 1024;
922 const PACKETS: u64 = 32;
923
924 let stamps: Vec<_> = (0..PACKETS)
926 .map(|packet| Timestamp::from_millis(packet * FRAMES * 1_000 / 44_100).unwrap())
927 .collect();
928 let read = pcm_gaps(44_100, 48_000, FRAMES as usize, &stamps).await;
929
930 assert_eq!(read.len(), stamps.len() + 1, "unexpected frames: {read:?}");
934
935 for pair in read.windows(2) {
938 let ends_at = pair[0].0 + (pair[0].1 as u128) * 1_000_000 / 48_000;
939 assert!(
940 pair[1].0.abs_diff(ends_at) <= 1_100,
941 "frames at {} and {} do not meet",
942 pair[0].0,
943 pair[1].0
944 );
945 }
946 }
947
948 #[tokio::test]
952 async fn a_lost_opus_packet_shorter_than_its_neighbour_is_a_gap() {
953 let input = Input {
954 format: Format::F32,
955 sample_rate: 48_000,
956 channels: 1,
957 };
958 let mut encoder = Encoder::new(&crate::encode::Config::new(input)).unwrap();
959 let catalog = encoder.catalog();
960
961 let mut broadcast = moq_net::broadcast::Info::new().produce();
962 let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
963 let subscriber = broadcast.consume();
964 let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
965 let mut consumer = Consumer::new(&subscriber, &catalog, "audio", Config::new())
966 .await
967 .unwrap();
968
969 let pcm = vec![0.25f32; encoder.frame_size()];
972 for timestamp in [
973 Timestamp::from_micros(0).unwrap(),
974 Timestamp::from_micros(22_500).unwrap(),
975 Timestamp::from_micros(42_500).unwrap(),
976 ] {
977 producer
978 .write(moq_mux::container::Frame {
979 timestamp,
980 duration: None,
981 payload: encoder.encode(&pcm).unwrap().payload,
982 keyframe: true,
983 })
984 .unwrap();
985 producer.cut(None).unwrap();
986 }
987
988 let first = consumer.read().await.unwrap().expect("decoded frame");
992 let frames = first.data.len() / size_of::<f32>();
993 assert!(frames < 960, "the pre-skip should be trimmed, got {frames} frames");
994
995 let second = consumer.read().await.unwrap().expect("decoded frame");
998 assert_eq!(second.timestamp.as_micros(), 22_500);
999 assert_eq!(second.data.len() / size_of::<f32>(), 960, "pre-skip was reapplied");
1000
1001 let third = consumer.read().await.unwrap().expect("decoded frame after gap");
1002 let second_frames = second.data.len() / size_of::<f32>();
1003 assert_eq!(
1004 third.timestamp,
1005 advance(second.timestamp, second_frames, 48_000).unwrap()
1006 );
1007 }
1008
1009 #[tokio::test]
1010 async fn latency_max_is_clamped_to_publisher_retention() {
1011 let mut broadcast = moq_net::broadcast::Info::new().produce();
1012 let info = hang::container::track_info().with_latency_max(std::time::Duration::from_millis(100));
1013 let _track = broadcast.create_track("audio", info).unwrap();
1014 let subscriber = broadcast.consume();
1015 let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 1);
1016
1017 let consumer = Consumer::new(
1018 &subscriber,
1019 &catalog,
1020 "audio",
1021 Config {
1022 latency_max: Some(std::time::Duration::from_millis(500)),
1023 ..Config::new()
1024 },
1025 )
1026 .await
1027 .unwrap();
1028
1029 assert_eq!(consumer.latency_max(), std::time::Duration::from_millis(100));
1030 }
1031
1032 #[tokio::test]
1036 async fn opus_pre_skip_does_not_leave_a_timestamp_hole() {
1037 let input = Input {
1038 format: Format::F32,
1039 sample_rate: 48_000,
1040 channels: 1,
1041 };
1042 let mut encoder = Encoder::new(&crate::encode::Config::new(input)).unwrap();
1043 let catalog = encoder.catalog();
1044
1045 let mut broadcast = moq_net::broadcast::Info::new().produce();
1046 let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
1047 let subscriber = broadcast.consume();
1048 let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
1049 let mut consumer = Consumer::new(&subscriber, &catalog, "audio", Config::new())
1050 .await
1051 .unwrap();
1052
1053 let pcm = vec![0.25f32; encoder.frame_size()];
1054 for packet in 0..2 {
1055 producer
1056 .write(moq_mux::container::Frame {
1057 timestamp: Timestamp::from_scale(packet * encoder.frame_size() as u64, 48_000).unwrap(),
1058 duration: None,
1059 payload: encoder.encode(&pcm).unwrap().payload,
1060 keyframe: true,
1061 })
1062 .unwrap();
1063 producer.cut(None).unwrap();
1064 }
1065
1066 let first = consumer.read().await.unwrap().expect("first decoded frame");
1067 let second = consumer.read().await.unwrap().expect("second decoded frame");
1068 let first_frames = first.data.len() / size_of::<f32>();
1069 let expected = advance(first.timestamp, first_frames, 48_000).unwrap();
1070 assert_eq!(second.timestamp, expected);
1071 }
1072
1073 #[tokio::test]
1074 async fn reads_the_container_the_catalog_declares() {
1075 let mut broadcast = moq_net::broadcast::Info::new().produce();
1076 let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
1077 let subscriber = broadcast.consume();
1078
1079 let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 1);
1080 catalog.container = hang::catalog::Container::Loc;
1081
1082 let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Loc);
1083 let mut consumer = Consumer::new(
1084 &subscriber,
1085 &catalog,
1086 "audio",
1087 Config {
1088 format: Format::F32,
1089 ..Config::new()
1090 },
1091 )
1092 .await
1093 .unwrap();
1094
1095 let samples = [0.25f32, -0.5, 0.75, -1.0];
1096 let payload: Vec<u8> = samples.iter().flat_map(|sample| sample.to_le_bytes()).collect();
1097 producer
1098 .write(moq_mux::container::Frame {
1099 timestamp: Timestamp::ZERO,
1100 duration: None,
1101 payload: payload.into(),
1102 keyframe: true,
1103 })
1104 .unwrap();
1105
1106 let frame = consumer.read().await.unwrap().expect("decoded frame");
1107 assert_eq!(
1108 Format::F32.as_interleaved_f32(&frame.data, 1).unwrap().as_ref(),
1109 samples
1110 );
1111 }
1112
1113 #[tokio::test]
1118 async fn decodes_a_cmaf_framed_track() {
1119 let input = Input {
1120 format: Format::F32,
1121 sample_rate: 48_000,
1122 channels: 2,
1123 };
1124
1125 let mut encoder = Encoder::new(&crate::encode::Config::new(input.clone())).unwrap();
1127 let mut catalog = encoder.catalog();
1128 let pcm = vec![0.0f32; encoder.frame_size() * encoder.codec_channels() as usize];
1129 let packet = encoder.encode(&pcm).unwrap();
1130
1131 let muxer = moq_mux::container::fmp4::Muxer::audio(&catalog).unwrap();
1133 let init = muxer.init().unwrap().expect("an out-of-band codec has an init segment");
1134 catalog.container = hang::catalog::Container::Cmaf { init };
1135
1136 let mut broadcast = moq_net::broadcast::Info::new().produce();
1137 let subscriber = broadcast.consume();
1138 let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
1139 let container = moq_mux::catalog::hang::Container::try_from(&catalog.container).unwrap();
1140 let mut producer = moq_mux::container::Producer::new(track, container);
1141
1142 let mut consumer = Consumer::new(&subscriber, &catalog, "audio", Config::new())
1143 .await
1144 .unwrap();
1145
1146 producer
1147 .write(moq_mux::container::Frame {
1148 timestamp: Timestamp::ZERO,
1149 payload: packet.payload,
1150 keyframe: true,
1151 duration: None,
1152 })
1153 .unwrap();
1154 producer.cut(None).unwrap();
1155
1156 let frame = consumer.read().await.unwrap().expect("decoded frame");
1160 assert_eq!(frame.timestamp.as_micros(), 0);
1163 let samples = Format::F32.as_interleaved_f32(&frame.data, 2).unwrap();
1164 assert_eq!(samples.len(), (960 - 312) * 2);
1165 }
1166}