1use std::time::Instant;
4
5use bytes::Bytes;
6
7use moq_mux::catalog::hang::CatalogExt;
8use moq_mux::container::Frame as MuxFrame;
9use moq_net::Timestamp;
10
11use super::encoded::Encoded;
12use super::encoder::{Encoder, Input, Settings};
13use crate::resample::{Resampler, remix, validate_remix};
14use crate::{Activity, Error, Frame};
15
16#[derive(Clone, Debug)]
21#[non_exhaustive]
22pub struct Options {
23 pub track: Option<String>,
27 pub settings: Settings,
29 pub bandwidth: moq_net::bandwidth::Allocator,
43}
44
45impl Default for Options {
46 fn default() -> Self {
47 Self {
48 track: None,
49 settings: Settings::default(),
50 bandwidth: moq_net::bandwidth::Allocator::unlimited(),
51 }
52 }
53}
54
55pub struct Producer<E: CatalogExt = ()> {
65 encoder: Encoder,
66 input: Input,
67 resampler: Option<Resampler>,
68 track: moq_mux::container::Producer<moq_mux::container::legacy::Wire, hang::catalog::AudioConfig>,
69 _ext: std::marker::PhantomData<fn() -> E>,
70 pending: Vec<f32>,
71 frames_produced: u64,
73 epoch_us: Option<u64>,
77 pending_discontinuity: bool,
79 decoder_boundary: bool,
81 activity: Activity,
83 finished: bool,
86}
87
88struct Terminal {
89 packets: Vec<Encoded>,
90 end: Timestamp,
91 start: Timestamp,
92 frame_size: usize,
93 codec_rate: u32,
94}
95
96pub(crate) struct Reserved<E: CatalogExt = ()> {
104 track: moq_mux::container::Producer<moq_mux::container::legacy::Wire, hang::catalog::AudioConfig>,
105 _ext: std::marker::PhantomData<fn() -> E>,
106}
107
108impl<E: CatalogExt> Reserved<E> {
109 pub(crate) fn new(
110 broadcast: &mut moq_net::broadcast::Producer,
111 catalog: moq_mux::catalog::Producer<E>,
112 options: &Options,
113 ) -> Result<Self, Error> {
114 let track = match &options.track {
115 Some(name) => broadcast.create_track(name.clone(), catalog.track_info(hang::catalog::PRIORITY.audio))?,
119 None => broadcast.unique_track(
122 &format!(".{}", options.settings.codec),
123 catalog.track_info(hang::catalog::PRIORITY.audio),
124 )?,
125 };
126 let track = catalog.audio(
127 track,
128 moq_mux::container::legacy::Wire(moq_mux::container::Kind::Audio),
129 None,
130 )?;
131
132 Ok(Self {
133 track,
134 _ext: std::marker::PhantomData,
135 })
136 }
137
138 pub(crate) fn register(&mut self, input: Input, options: &Options) -> Result<Registered, Error> {
143 validate_remix(input.layout, options.settings.layout)?;
144 let encoder = Encoder::new(&options.settings)?;
145
146 let resampler = if input.sample_rate == encoder.codec_rate() {
147 None
148 } else {
149 let chunk_frames =
152 ((input.sample_rate as u128 * encoder.settings().frame_duration.as_micros()) / 1_000_000) as usize;
153 Some(Resampler::new(
154 input.sample_rate,
155 encoder.codec_rate(),
156 encoder.codec_channels(),
157 chunk_frames,
158 )?)
159 };
160
161 self.track.set(encoder.catalog())?;
162
163 Ok(Registered {
164 encoder,
165 input,
166 resampler,
167 })
168 }
169
170 pub(crate) fn encode(self, registered: Registered) -> Producer<E> {
172 Producer {
173 encoder: registered.encoder,
174 input: registered.input,
175 resampler: registered.resampler,
176 track: self.track,
177 _ext: self._ext,
178 pending: Vec::new(),
179 frames_produced: 0,
180 epoch_us: None,
181 pending_discontinuity: false,
182 decoder_boundary: true,
183 activity: Activity::Active,
184 finished: false,
185 }
186 }
187}
188
189pub(crate) struct Registered {
194 encoder: Encoder,
195 input: Input,
196 resampler: Option<Resampler>,
197}
198
199#[cfg(feature = "capture")]
201impl<E: CatalogExt> Reserved<E> {
202 pub(crate) fn name(&self) -> &str {
204 self.track.name()
205 }
206
207 pub(crate) fn track(&self) -> &moq_net::track::Producer {
209 self.track.track()
210 }
211
212 pub(crate) fn finish(&mut self) -> Result<(), Error> {
214 self.track.finish()?;
215 Ok(())
216 }
217
218 pub(crate) fn abort(self, err: moq_net::Error) {
220 self.track.abort(err);
221 }
222}
223
224impl<E: CatalogExt> Producer<E> {
225 pub fn new(
228 broadcast: &mut moq_net::broadcast::Producer,
229 catalog: moq_mux::catalog::Producer<E>,
230 input: Input,
231 options: &Options,
232 ) -> Result<Self, Error> {
233 let mut reserved = Reserved::new(broadcast, catalog, options)?;
234 let registered = reserved.register(input, options)?;
235 Ok(reserved.encode(registered))
236 }
237
238 pub fn track_name(&self) -> &str {
240 self.track.name()
241 }
242
243 pub fn demand(&self) -> moq_net::track::Demand {
247 self.track.track().demand()
248 }
249
250 #[cfg(feature = "capture")]
251 pub(crate) fn track(&self) -> &moq_net::track::Producer {
252 self.track.track()
253 }
254
255 pub fn activity(&self) -> Activity {
264 self.activity
265 }
266
267 pub fn bitrate(&self) -> moq_net::bandwidth::Rate {
269 self.encoder.bitrate()
270 }
271
272 pub fn set_bitrate(&mut self, bitrate: moq_net::bandwidth::Rate) -> Result<(), Error> {
274 self.encoder.set_bitrate(bitrate)
275 }
276
277 pub fn reset_epoch(&mut self) {
285 if self.encoder.started() && !self.decoder_boundary {
286 self.pending_discontinuity = true;
287 }
288 self.reset_state();
289 }
290
291 fn reset_state(&mut self) {
292 self.epoch_us = None;
293 self.activity = Activity::Active;
294 self.frames_produced = 0;
295 self.pending.clear();
296 self.encoder.reset();
297 if let Some(resampler) = self.resampler.as_mut() {
302 resampler.reset();
303 }
304 }
305
306 pub fn write(&mut self, frame: &Frame) -> Result<(), Error> {
324 if self.finished {
325 return Err(moq_net::Error::Closed.into());
326 }
327 if self.pending_discontinuity {
328 self.track.discontinuity()?;
329 self.pending_discontinuity = false;
330 self.decoder_boundary = true;
331 }
332
333 let timestamp_us = u64::try_from(frame.timestamp.as_micros())
334 .map_err(|_| Error::Unsupported(format!("frame timestamp {:?} out of range", frame.timestamp)))?;
335 let epoch_us = *self.epoch_us.get_or_insert(timestamp_us);
336
337 let input = &self.input;
338 let (format, channels) = (input.format, input.layout.channels());
339 let pcm = format.as_interleaved_f32(frame.data.as_ref(), channels)?;
340 let pcm = remix(&pcm, input.layout, self.encoder.settings().layout)?;
341 let pcm: Vec<f32> = match self.resampler.as_mut() {
342 Some(r) => r.process(&pcm, frame.timestamp)?,
343 None => pcm,
344 };
345
346 self.pending.extend(pcm);
347
348 self.publish_full_frames(epoch_us)
349 }
350
351 fn publish_full_frames(&mut self, epoch_us: u64) -> Result<(), Error> {
354 let frame_samples = self.encoder.frame_size() * self.encoder.codec_channels() as usize;
355 while self.pending.len() >= frame_samples {
356 let chunk: Vec<f32> = self.pending.drain(..frame_samples).collect();
357 let packet = self.encoder.encode(&chunk)?;
358
359 let timestamp = Self::timestamp(epoch_us, self.frames_produced, self.encoder.codec_rate())?;
360 self.frames_produced += self.encoder.frame_size() as u64;
361 self.activity = packet.activity;
362 Self::publish(&mut self.track, packet, timestamp)?;
363 self.decoder_boundary = false;
364 }
365
366 Ok(())
367 }
368
369 fn timestamp(epoch_us: u64, frames_produced: u64, codec_rate: u32) -> Result<Timestamp, Error> {
371 let offset_us = (frames_produced * 1_000_000) / codec_rate as u64;
372 Ok(Timestamp::from_micros(epoch_us + offset_us)?)
373 }
374
375 fn publish(
376 track: &mut moq_mux::container::Producer<moq_mux::container::legacy::Wire, hang::catalog::AudioConfig>,
377 encoded: Encoded,
378 timestamp: Timestamp,
379 ) -> Result<(), Error> {
380 let mux_frame = MuxFrame {
384 timestamp,
385 payload: encoded.payload,
386 keyframe: true,
387 duration: None,
388 };
389 track.write(mux_frame)?;
390 track.cut(None)?;
394 track.flush(timestamp, Instant::now())?;
395 Ok(())
396 }
397
398 fn publish_terminal(
400 track: &mut moq_mux::container::Producer<moq_mux::container::legacy::Wire, hang::catalog::AudioConfig>,
401 terminal: Terminal,
402 ) -> Result<(), Error> {
403 track.write(MuxFrame {
404 timestamp: terminal.end,
405 payload: Bytes::new(),
406 keyframe: true,
407 duration: None,
408 })?;
409
410 for (index, packet) in terminal.packets.into_iter().enumerate() {
411 let offset = Timestamp::from_scale((index * terminal.frame_size) as u64, terminal.codec_rate as u64)?
412 .convert(terminal.start.scale())?;
413 let timestamp = terminal.start.checked_add(offset)?;
414 track.write(MuxFrame {
415 timestamp,
416 payload: packet.payload,
417 keyframe: false,
418 duration: None,
419 })?;
420 track.flush(timestamp, Instant::now())?;
421 }
422
423 track.cut(Some(terminal.end))?;
424 Ok(())
425 }
426
427 pub fn discontinuity(&mut self) -> Result<(), Error> {
434 self.track.discontinuity()?;
435 self.pending_discontinuity = false;
436 self.decoder_boundary = true;
437 self.reset_state();
438 Ok(())
439 }
440
441 pub fn finish(&mut self) -> Result<(), Error> {
448 if self.finished {
449 return Ok(());
450 }
451 if let Some(resampler) = self.resampler.take() {
455 self.pending.extend(resampler.flush()?);
456 }
457
458 let epoch_us = self.epoch_us.unwrap_or(0);
461 self.publish_full_frames(epoch_us)?;
462
463 let frame_size = self.encoder.frame_size();
464 let codec_rate = self.encoder.codec_rate();
465 let channels = self.encoder.codec_channels() as usize;
466 let source_frames = self.pending.len() / channels;
467 let start = Self::timestamp(epoch_us, self.frames_produced, codec_rate)?;
468 let end = Self::timestamp(epoch_us, self.frames_produced + source_frames as u64, codec_rate)?;
469 let finish = self.encoder.drain(&self.pending)?;
470 let discard_padding = finish.discard_padding();
471 let packets = finish.into_packets();
472
473 if discard_padding > 0 {
474 Self::publish_terminal(
475 &mut self.track,
476 Terminal {
477 packets,
478 end,
479 start,
480 frame_size,
481 codec_rate,
482 },
483 )?;
484 } else {
485 for packet in packets {
486 let timestamp = Self::timestamp(epoch_us, self.frames_produced, codec_rate)?;
487 self.activity = packet.activity;
488 Self::publish(&mut self.track, packet, timestamp)?;
489 self.frames_produced += frame_size as u64;
490 }
491 }
492
493 self.track.finish()?;
494 self.finished = true;
495 Ok(())
496 }
497
498 pub fn abort(self, err: moq_net::Error) {
503 self.track.abort(err);
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use std::time::Duration;
510
511 use super::*;
512 use crate::decode::{Consumer as AudioConsumer, Options as DecodeOptions};
513 use crate::{Activity, Format, Layout};
514
515 #[tokio::test]
516 async fn demand_follows_subscribers_and_closes_with_the_producer() {
517 let mut broadcast = moq_net::broadcast::Info::new().produce();
518 let consumer = broadcast.consume();
519 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
520 let options = Options {
521 track: Some("audio".into()),
522 ..Options::default()
523 };
524 let mut producer = Producer::new(&mut broadcast, catalog, Input::default(), &options).unwrap();
525 let demand = producer.demand();
526
527 assert_eq!(demand.name(), "audio");
528 assert!(!demand.is_used());
529 let subscriber = consumer.track("audio").unwrap().subscribe(None).await.unwrap();
530 tokio::time::timeout(Duration::from_secs(1), demand.used())
531 .await
532 .expect("subscription demand")
533 .unwrap();
534 assert!(demand.is_used());
535
536 drop(subscriber);
537 drop(consumer);
538 tokio::time::timeout(Duration::from_secs(1), demand.unused())
539 .await
540 .expect("subscription released")
541 .unwrap();
542 assert!(!demand.is_used());
543
544 producer.finish().unwrap();
545 drop(producer);
546 let closed = tokio::time::timeout(Duration::from_secs(1), demand.closed())
547 .await
548 .expect("producer closed");
549 assert!(matches!(closed, moq_net::Error::Dropped));
550 }
551
552 #[tokio::test]
554 async fn finish_publishes_the_opus_lookahead_tail() {
555 for frames in [960, 860] {
556 let input = Input {
557 format: Format::F32,
558 sample_rate: 48_000,
559 layout: Layout::Mono,
560 };
561 let options = Options {
562 track: Some("audio".to_string()),
563 settings: Settings {
564 layout: Layout::Mono,
565 bitrate: Some(moq_net::bandwidth::Rate::from_bps(128_000)),
566 ..Settings::default()
567 },
568 ..Options::default()
569 };
570 let decoder_config = Encoder::new(&options.settings).unwrap().catalog();
571
572 let mut broadcast = moq_net::broadcast::Info::new().produce();
573 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
574 let consumer = broadcast.consume();
575 let mut producer = Producer::new(&mut broadcast, catalog, input, &options).unwrap();
576 let mut audio = AudioConsumer::new(
577 &consumer,
578 &decoder_config,
579 "audio",
580 DecodeOptions {
581 max_age: Duration::from_secs(1),
582 ..DecodeOptions::new()
583 },
584 )
585 .await
586 .unwrap();
587
588 let mut pcm = vec![0.0f32; frames];
589 let impulse = pcm.len() - 100;
590 pcm[impulse] = 1.0;
591 let data: Vec<u8> = pcm.iter().flat_map(|sample| sample.to_le_bytes()).collect();
592 producer.write(&Frame::new(Bytes::from(data), Timestamp::ZERO)).unwrap();
593 producer.finish().unwrap();
594
595 let mut decoded = Vec::new();
596 while let Some(frame) = audio.read().await.unwrap() {
597 let pcm = Format::F32.as_interleaved_f32(&frame.data, 1).unwrap();
598 decoded.extend_from_slice(&pcm);
599 }
600 assert_eq!(decoded.len(), frames, "terminal padding extended the source");
601 let peak = decoded.iter().fold(0.0f32, |peak, sample| peak.max(sample.abs()));
602 assert!(peak > 0.1, "the {frames}-frame Opus tail lost the impulse: peak {peak}");
603 }
604 }
605
606 #[tokio::test]
610 async fn finish_publishes_the_resampled_tail() {
611 let input = Input {
612 format: Format::F32,
613 sample_rate: 44_100,
614 layout: Layout::Mono,
615 };
616
617 let mut broadcast = moq_net::broadcast::Info::new().produce();
618 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
619 let consumer = broadcast.consume();
620 let options = Options {
621 track: Some("audio".to_string()),
622 settings: Settings::new(48_000, Layout::Mono),
623 ..Options::default()
624 };
625 let mut producer = Producer::new(&mut broadcast, catalog, input.clone(), &options).unwrap();
626
627 let mut track = moq_mux::container::Consumer::new(
629 consumer
630 .track("audio")
631 .unwrap()
632 .subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(1)))
633 .await
634 .unwrap(),
635 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Audio),
636 );
637
638 let data: Vec<u8> = vec![0.25f32; 8_838].iter().flat_map(|s| s.to_le_bytes()).collect();
642 producer
643 .write(&Frame::new(data.into(), moq_net::Timestamp::ZERO))
644 .unwrap();
645 producer.finish().unwrap();
646
647 let mut packets = 0;
648 while track.read().await.unwrap().is_some() {
649 packets += 1;
650 }
651 assert_eq!(packets, 11);
652 }
653
654 #[tokio::test]
658 async fn reset_epoch_drops_the_resampler_buffer_too() {
659 let input = Input {
660 format: Format::F32,
661 sample_rate: 44_100,
662 layout: Layout::Mono,
663 };
664
665 let mut broadcast = moq_net::broadcast::Info::new().produce();
666 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
667 let consumer = broadcast.consume();
668 let options = Options {
669 track: Some("audio".to_string()),
670 ..Options::default()
671 };
672 let mut producer = Producer::new(&mut broadcast, catalog, input.clone(), &options).unwrap();
673
674 let mut track = moq_mux::container::Consumer::new(
675 consumer
676 .track("audio")
677 .unwrap()
678 .subscribe(moq_net::track::Subscription::default())
679 .await
680 .unwrap(),
681 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Audio),
682 );
683
684 let data: Vec<u8> = vec![0.25f32; 441].iter().flat_map(|s| s.to_le_bytes()).collect();
686 producer
687 .write(&Frame::new(data.into(), moq_net::Timestamp::ZERO))
688 .unwrap();
689
690 producer.reset_epoch();
691 producer.finish().unwrap();
692
693 assert!(track.read().await.unwrap().is_none());
695 }
696
697 #[tokio::test]
699 async fn reset_epoch_drops_the_encoder_lookahead() {
700 let mut broadcast = moq_net::broadcast::Info::new().produce();
701 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
702 let consumer = broadcast.consume();
703 let options = Options {
704 track: Some("audio".to_string()),
705 ..Options::default()
706 };
707 let mut producer = Producer::new(
708 &mut broadcast,
709 catalog,
710 Input {
711 layout: Layout::Mono,
712 ..Input::default()
713 },
714 &options,
715 )
716 .unwrap();
717 let mut track = moq_mux::container::Consumer::new(
718 consumer
719 .track("audio")
720 .unwrap()
721 .subscribe(moq_net::track::Subscription::default())
722 .await
723 .unwrap(),
724 moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Audio),
725 );
726
727 producer.write(&full_frame(1_000_000)).unwrap();
728 producer.reset_epoch();
729 producer.finish().unwrap();
730
731 assert!(track.read().await.unwrap().is_some());
732 assert!(track.read().await.unwrap().is_none());
733 }
734
735 #[tokio::test]
737 async fn reset_epoch_restarts_the_decoder() {
738 let input = Input {
739 format: Format::F32,
740 sample_rate: 48_000,
741 layout: Layout::Mono,
742 };
743 let options = Options {
744 track: Some("audio".to_string()),
745 settings: Settings::new(48_000, Layout::Mono),
746 ..Options::default()
747 };
748 let decoder_config = Encoder::new(&options.settings).unwrap().catalog();
749
750 let mut broadcast = moq_net::broadcast::Info::new().produce();
751 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
752 let subscriber = broadcast.consume();
753 let mut producer = Producer::new(&mut broadcast, catalog, input, &options).unwrap();
754 let mut audio = AudioConsumer::new(
755 &subscriber,
756 &decoder_config,
757 "audio",
758 DecodeOptions {
759 max_age: Duration::from_millis(500),
760 ..DecodeOptions::new()
761 },
762 )
763 .await
764 .unwrap();
765
766 producer.write(&full_frame(0)).unwrap();
767 let first = audio.read().await.unwrap().expect("first epoch packet");
768 assert_eq!(first.data.len() / size_of::<f32>(), 960 - 312);
769
770 producer.reset_epoch();
771 producer.write(&full_frame(1_000_000)).unwrap();
772 producer.finish().unwrap();
773
774 let mut resumed_frames = 0;
775 while let Some(frame) = audio.read().await.unwrap() {
776 if frame.timestamp.as_micros() >= 1_000_000 {
777 resumed_frames += frame.data.len() / size_of::<f32>();
778 }
779 }
780 assert!(resumed_frames > 0, "the resumed epoch still decodes");
781 }
782
783 #[tokio::test]
784 async fn producer_and_consumer_keep_activity_on_the_audio_stream() {
785 let input = Input {
786 format: Format::F32,
787 sample_rate: 48_000,
788 layout: Layout::Mono,
789 };
790 let options = Options {
791 track: Some("audio".to_string()),
792 settings: Settings {
793 layout: Layout::Mono,
794 bitrate: Some(moq_net::bandwidth::Rate::from_bps(24_000)),
795 dtx: true,
796 ..Settings::default()
797 },
798 ..Options::default()
799 };
800 let decoder_config = Encoder::new(&options.settings).unwrap().catalog();
801
802 let mut broadcast = moq_net::broadcast::Info::new().produce();
803 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
804 let subscriber = broadcast.consume();
805 let mut producer = Producer::new(&mut broadcast, catalog, input, &options).unwrap();
806 let mut consumer = AudioConsumer::new(&subscriber, &decoder_config, "audio", DecodeOptions::new())
807 .await
808 .unwrap();
809
810 let silence = vec![0.0; 960];
811 let mut entered_dtx = false;
812 for index in 0..100 {
813 producer.write(&pcm_frame(&silence, index * 20_000)).unwrap();
814 let consumed = consumer.read().await.unwrap().expect("one decoded frame");
815 assert_eq!(producer.activity(), consumed.activity);
816 if consumed.activity.is_dtx() {
817 entered_dtx = true;
818 break;
819 }
820 }
821 assert!(entered_dtx, "silence should enter Opus DTX");
822
823 let active: Vec<f32> = (0..960)
824 .map(|sample| {
825 let phase = sample as f32 * 440.0 * 2.0 * std::f32::consts::PI / 48_000.0;
826 phase.sin() * 0.5
827 })
828 .collect();
829 producer.write(&pcm_frame(&active, 2_000_000)).unwrap();
830 let consumed = consumer.read().await.unwrap().expect("one decoded frame");
831 assert_eq!(producer.activity(), Activity::Active);
832 assert_eq!(consumed.activity, Activity::Active);
833 }
834
835 fn full_frame(timestamp_us: u64) -> Frame {
838 pcm_frame(&vec![0.1; 960], timestamp_us)
839 }
840
841 fn pcm_frame(samples: &[f32], timestamp_us: u64) -> Frame {
842 let data: Vec<u8> = samples.iter().flat_map(|sample| sample.to_le_bytes()).collect();
843 Frame::new(Bytes::from(data), Timestamp::from_micros(timestamp_us).unwrap())
844 }
845
846 async fn published_pts(frames: &[Frame], reset_before: Option<usize>) -> Vec<u128> {
850 let mut broadcast = moq_net::broadcast::Info::new().produce();
851 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
852 let consumer = broadcast.consume();
853
854 let input = Input {
857 format: Format::F32,
858 sample_rate: 48_000,
859 layout: Layout::Mono,
860 };
861 let options = Options {
862 track: Some("audio".to_string()),
863 ..Options::default()
864 };
865 let mut producer = Producer::new(&mut broadcast, catalog, input, &options).unwrap();
866
867 let track = consumer.track("audio").unwrap().subscribe(None).await.unwrap();
868 let mut reader =
869 moq_mux::container::Consumer::new(track, moq_mux::container::legacy::Wire(moq_mux::container::Kind::Audio));
870
871 let mut pts = Vec::new();
872 for (i, frame) in frames.iter().enumerate() {
873 if reset_before == Some(i) {
874 producer.reset_epoch();
875 }
876 producer.write(frame).unwrap();
877 let read = reader.read().await.unwrap().expect("a packet per full frame");
878 pts.push(read.timestamp.as_micros());
879 }
880 pts
881 }
882
883 #[tokio::test]
884 async fn epoch_anchors_to_first_frame_timestamp() {
885 let pts = published_pts(&[full_frame(1_000_000)], None).await;
888 assert_eq!(pts, vec![1_000_000]);
889 }
890
891 #[tokio::test]
898 async fn resampling_does_not_shift_the_first_pts() {
899 let mut broadcast = moq_net::broadcast::Info::new().produce();
900 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
901 let consumer = broadcast.consume();
902
903 let input = Input {
905 format: Format::F32,
906 sample_rate: 44_100,
907 layout: Layout::Mono,
908 };
909 let options = Options {
910 track: Some("audio".to_string()),
911 ..Options::default()
912 };
913 let mut producer = Producer::new(&mut broadcast, catalog, input, &options).unwrap();
914
915 let track = consumer
918 .track("audio")
919 .unwrap()
920 .subscribe(moq_net::track::Subscription::default().with_max_age(Duration::from_secs(1)))
921 .await
922 .unwrap();
923 let mut reader =
924 moq_mux::container::Consumer::new(track, moq_mux::container::legacy::Wire(moq_mux::container::Kind::Audio));
925
926 producer.write(&pcm_frame(&vec![0.1; 44_100], 1_000_000)).unwrap();
928
929 let first = reader.read().await.unwrap().expect("a packet");
930 assert_eq!(first.timestamp.as_micros(), 1_000_000);
931 }
932
933 #[tokio::test]
934 async fn pts_advances_by_frame_duration_ignoring_later_timestamps() {
935 let pts = published_pts(&[full_frame(1_000), full_frame(999_999)], None).await;
938 assert_eq!(pts, vec![1_000, 1_000 + 20_000]);
939 }
940
941 #[tokio::test]
942 async fn reset_epoch_reanchors_so_the_gap_lands_in_pts() {
943 let pts = published_pts(&[full_frame(0), full_frame(5_000_000)], Some(1)).await;
946 assert_eq!(pts, vec![0, 5_000_000]);
947 }
948
949 #[tokio::test]
951 async fn abort_after_finish() {
952 let mut broadcast = moq_net::broadcast::Info::new().produce();
953 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
954 let consumer = broadcast.consume();
955 let options = Options {
956 track: Some("audio".to_string()),
957 ..Options::default()
958 };
959 let mut producer = Producer::new(
960 &mut broadcast,
961 catalog,
962 Input {
963 layout: Layout::Mono,
964 ..Input::default()
965 },
966 &options,
967 )
968 .unwrap();
969 let mut track = moq_mux::container::Consumer::new(
970 consumer
971 .track("audio")
972 .unwrap()
973 .subscribe(moq_net::track::Subscription::default())
974 .await
975 .unwrap(),
976 moq_mux::container::legacy::Wire(moq_mux::container::Kind::Audio),
977 );
978
979 producer.write(&full_frame(0)).unwrap();
980 producer.finish().unwrap();
981 assert!(track.read().await.unwrap().is_some());
982 producer.abort(moq_net::Error::Cancel);
983 }
984
985 #[tokio::test]
988 async fn write_after_finish_is_closed() {
989 let mut broadcast = moq_net::broadcast::Info::new().produce();
990 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
991 let options = Options {
992 track: Some("audio".to_string()),
993 ..Options::default()
994 };
995 let mut producer = Producer::new(
996 &mut broadcast,
997 catalog,
998 Input {
999 layout: Layout::Mono,
1000 ..Input::default()
1001 },
1002 &options,
1003 )
1004 .unwrap();
1005
1006 producer.finish().unwrap();
1007 let err = producer.write(&pcm_frame(&[0.1; 100], 0)).unwrap_err();
1008 assert!(matches!(err, Error::Net(moq_net::Error::Closed)));
1009 producer.abort(moq_net::Error::Cancel);
1010 }
1011
1012 #[tokio::test]
1016 async fn default_options_derive_the_track_name() {
1017 let mut broadcast = moq_net::broadcast::Info::new().produce();
1018 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1019
1020 let first = Producer::new(&mut broadcast, catalog.clone(), Input::default(), &Options::default()).unwrap();
1021 assert_eq!(first.track_name(), "0.opus");
1022
1023 let second = Producer::new(&mut broadcast, catalog, Input::default(), &Options::default()).unwrap();
1024 assert_eq!(second.track_name(), "1.opus");
1025 }
1026}