Skip to main content

moq_audio/encode/
producer.rs

1//! Encode raw PCM and publish it as a moq audio track.
2
3use 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/// Encode and publication policy for [`Producer`].
17///
18/// `#[non_exhaustive]`: construct via [`Options::default`] and set fields, so
19/// new knobs can be added without breaking callers.
20#[derive(Clone, Debug)]
21#[non_exhaustive]
22pub struct Options {
23	/// Track name to publish under. `None` derives a unique one from the codec
24	/// (`0.opus`, then `1.opus`, ...), matching how the video side names its
25	/// track. Subscribers find it through the catalog either way.
26	pub track: Option<String>,
27	/// Codec input and encoded output settings.
28	pub settings: Settings,
29	/// The connection's bandwidth, as an allocator over
30	/// [`Session::send_bandwidth`](moq_net::Session::send_bandwidth).
31	///
32	/// The audio track reserves its bitrate against it, so the video encoder sharing
33	/// the connection sizes itself against what's actually left rather than against
34	/// the whole uplink. Pass the same allocator to both.
35	///
36	/// Defaults to [`Allocator::unlimited`](moq_net::bandwidth::Allocator::unlimited),
37	/// which reserves nothing and leaves every sender at its configured rate.
38	///
39	/// Audio reserves but does not follow its share: Opus can retune live and PCM
40	/// can't at all, and at `hang`'s priorities audio outranks video, so it is only
41	/// ever squeezed on a link that can't carry audio alone.
42	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
55/// Encode raw PCM and publish it as a moq-mux audio track.
56///
57/// The input PCM layout is fixed at construction via [`Input`]; the codec
58/// settings via [`Options`]. Subsequent [`write`](Self::write) calls just pass a
59/// [`Frame`]: payload bytes and a timestamp.
60///
61/// The catalog rendition is registered at construction (not on first write), so
62/// a subscriber that opens the catalog before any frames arrive still sees the
63/// track.
64pub 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	/// Samples emitted since the current epoch (reset by [`reset_epoch`](Self::reset_epoch)).
72	frames_produced: u64,
73	/// Wall-clock anchor in microseconds, taken from the first frame after each
74	/// (re)start. Emitted PTS = `epoch + frames_produced / codec_rate`. `None`
75	/// until the first write so the next frame re-anchors to its timestamp.
76	epoch_us: Option<u64>,
77	/// An encoder reset that still needs a marker group before its next packet.
78	pending_discontinuity: bool,
79	/// Whether a marker group already separates the next packet from prior codec state.
80	decoder_boundary: bool,
81	/// How the encoder classified the packet it published most recently.
82	activity: Activity,
83	/// Set after a successful [`finish`](Self::finish). Writes then fail with
84	/// [`moq_net::Error::Closed`]; [`abort`](Self::abort) can still run.
85	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
96/// A published track whose PCM layout is not known yet.
97///
98/// The track exists in the broadcast immediately, so its name and subscriber
99/// state are available, while the catalog rendition waits for
100/// [`encode`](Self::encode) to supply the layout it describes. Unlike a catalog
101/// [`Reserved`](moq_mux::catalog::Reserved) this does not withhold the catalog:
102/// subscribers see the broadcast without this rendition until it resolves.
103pub(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			// The catalog's info carries the microsecond timescale audio hang frames stamp, so
116			// Lite05 subscribers know what scale to expect and the model layer accepts
117			// Frame::timestamp on append, plus whatever retention the broadcast declared.
118			Some(name) => broadcast.create_track(name.clone(), catalog.track_info(hang::catalog::PRIORITY.audio))?,
119			// Mirrors the video side, which derives a unique name from the codec
120			// rather than making every caller invent one.
121			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	/// Build the encoder for `input` and register the rendition describing it.
139	///
140	/// Separate from [`encode`](Self::encode), which cannot fail, so a layout the
141	/// codec rejects leaves the reservation intact for another input.
142	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			// Use microsecond precision so 2.5 ms frame_duration (supported by
150			// libopus) doesn't truncate to 2 ms.
151			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	/// Spend the reservation on a registered encoder, publishing through the track.
171	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
189/// A registered rendition, waiting for its [`Reserved`] to be spent on it.
190///
191/// Proof that [`Reserved::register`] succeeded, so [`Reserved::encode`] takes no
192/// fallible step and cannot strand the track it consumes.
193pub(crate) struct Registered {
194	encoder: Encoder,
195	input: Input,
196	resampler: Option<Resampler>,
197}
198
199/// What a capture publication needs while its layout is still undiscovered.
200#[cfg(feature = "capture")]
201impl<E: CatalogExt> Reserved<E> {
202	/// The resolved track name, available before the layout is.
203	pub(crate) fn name(&self) -> &str {
204		self.track.name()
205	}
206
207	/// The underlying track producer, e.g. to watch subscriber state.
208	pub(crate) fn track(&self) -> &moq_net::track::Producer {
209		self.track.track()
210	}
211
212	/// Finalize a track that never got a rendition.
213	pub(crate) fn finish(&mut self) -> Result<(), Error> {
214		self.track.finish()?;
215		Ok(())
216	}
217
218	/// Abort a track that never got a rendition, so subscribers see `err`.
219	pub(crate) fn abort(self, err: moq_net::Error) {
220		self.track.abort(err);
221	}
222}
223
224impl<E: CatalogExt> Producer<E> {
225	/// Publish a track encoding `input` into `broadcast`, registering its
226	/// rendition in `catalog` immediately.
227	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	/// The name of the published track, which is [`Options::track`] resolved.
239	pub fn track_name(&self) -> &str {
240		self.track.name()
241	}
242
243	/// A watch-only handle to the track's subscriber demand, created eagerly so
244	/// subscription state is observable before any frames arrive. Watch it via
245	/// [`used`](moq_net::track::Demand::used) / [`unused`](moq_net::track::Demand::unused).
246	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	/// Whether the packet published most recently coded audio, or withheld it
256	/// because the input was silent.
257	///
258	/// A local "am I talking" indicator without running a second voice detector
259	/// over the microphone, though a silent run is punctuated by coded frames
260	/// that read [`Activity::Active`], so hold the indicator across those rather
261	/// than following it packet by packet. [`Activity::Active`] until the first
262	/// packet, and for codecs without a discontinuous mode.
263	pub fn activity(&self) -> Activity {
264		self.activity
265	}
266
267	/// Current encoder target bitrate.
268	pub fn bitrate(&self) -> moq_net::bandwidth::Rate {
269		self.encoder.bitrate()
270	}
271
272	/// Retune the live encoder to `bitrate`.
273	pub fn set_bitrate(&mut self, bitrate: moq_net::bandwidth::Rate) -> Result<(), Error> {
274		self.encoder.set_bitrate(bitrate)
275	}
276
277	/// Re-anchor the timeline to the next frame's timestamp, dropping any
278	/// buffered samples. Call this when resuming after an idle gap (e.g. a
279	/// released-then-reopened microphone) so the gap appears in the PTS and
280	/// audio stays aligned with a wall-clock video track, rather than the gap
281	/// being compressed out by the running sample count. Mirrors moq-boy's
282	/// `reset_epoch`. If the codec had started, a marker group is published before
283	/// the next packet so subscribers jump the playhead.
284	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		// The resampler holds samples of its own, plus filter state primed by them.
298		// Left alone, `finish` would flush that pre-reset audio onto the track
299		// (stamped at an epoch that no longer exists), and the next write would run
300		// the new audio through a filter still ringing with the old.
301		if let Some(resampler) = self.resampler.as_mut() {
302			resampler.reset();
303		}
304	}
305
306	/// Push one [`Frame`] of PCM in the layout declared by [`Input`]. Encodes and
307	/// publishes as many packets as the input contains; any partial trailing
308	/// frame is carried to the next call.
309	///
310	/// The first frame after construction (or [`reset_epoch`](Self::reset_epoch))
311	/// anchors the timeline: its timestamp becomes the epoch, and emitted PTS
312	/// then advances purely by the running sample count, so subsequent frames'
313	/// timestamps are ignored. An idle gap is only reflected in the PTS if you
314	/// call [`reset_epoch`](Self::reset_epoch) on resume (which re-anchors from
315	/// the next frame's wall-clock stamp); writing straight across a gap without
316	/// resetting compresses it out.
317	///
318	/// [`Frame::activity`] is ignored: the encoder classifies what it actually
319	/// produced, which [`activity`](Self::activity) reports.
320	///
321	/// Writes after a successful [`finish`](Self::finish) fail with
322	/// [`moq_net::Error::Closed`].
323	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	/// Encode and publish every full frame in `pending`, keeping any partial
352	/// trailing frame for the next call.
353	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	/// PTS of the next frame: the epoch plus the samples emitted since it.
370	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		// Publish each audio packet as its own moq-lite group: write it as a keyframe, then cut
381		// (below) so the relay forwards it without waiting for the next. Codecs can recover
382		// independently after a dropped group.
383		let mux_frame = MuxFrame {
384			timestamp,
385			payload: encoded.payload,
386			keyframe: true,
387			duration: None,
388		};
389		track.write(mux_frame)?;
390		// No boundary to give: the next packet bounds this one, and Opus frames have a
391		// deterministic duration anyway. Cut before observing the flush so a failed observation
392		// never leaves the group open.
393		track.cut(None)?;
394		track.flush(timestamp, Instant::now())?;
395		Ok(())
396	}
397
398	/// Publish terminal packets after an empty frame that carries their logical endpoint.
399	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	/// Mark a break in the published timeline and reset codec state.
428	///
429	/// Call this when capture stops rather than merely gapping between packets: going idle,
430	/// switching source, or anything else that resumes on a re-anchored epoch. Buffered samples
431	/// are dropped, and the next frame anchors a fresh codec epoch. See
432	/// [`Producer::discontinuity`](moq_mux::container::Producer::discontinuity).
433	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	/// Flush pending samples, resampler output, and codec lookahead, then finalize
442	/// the track.
443	///
444	/// Borrows rather than consumes, so a later [`abort`](Self::abort) can still
445	/// run after a successful finish. Writes after this fail with
446	/// [`moq_net::Error::Closed`].
447	pub fn finish(&mut self) -> Result<(), Error> {
448		if self.finished {
449			return Ok(());
450		}
451		// Whatever the resampler still holds belongs to this track: its last partial
452		// chunk, plus the audio its filter is running behind on. Dropping it here
453		// would publish a track that ends before its source did.
454		if let Some(resampler) = self.resampler.take() {
455			self.pending.extend(resampler.flush()?);
456		}
457
458		// The drained resampler tail can span multiple frames. Publish those first
459		// so only the final partial frame reaches the encoder's terminal drain.
460		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	/// Abort the track with `err` instead of finishing it, so subscribers see the
499	/// real cause rather than [`moq_net::Error::Dropped`]. Pending samples are dropped.
500	///
501	/// Consumes the producer. Still callable after [`finish`](Self::finish).
502	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	/// Terminal Opus lookahead samples survive both exact-frame and partial-frame input.
553	#[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	/// A resampled publisher used to end its track early: `finish` flushed the
607	/// encoder's own buffer but left the resampler holding its last partial chunk,
608	/// plus the audio its filter runs behind on.
609	#[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		// Subscribe before the track ends, or there is nothing left to subscribe to.
628		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		// Chosen so the tail decides a whole packet: 8838 frames at 44.1 kHz is ~9620
639		// at 48 kHz, just past ten 960-sample Opus frames. Losing the resampler's
640		// remainder and its filter delay drops back under ten, costing a packet.
641		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	/// `reset_epoch` promises to drop buffered samples, and the resampler buffers
655	/// samples of its own. Leaving those behind let `finish` flush pre-reset audio
656	/// onto the track, stamped at an epoch that no longer exists.
657	#[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		// Too little to publish a packet, so it all sits in the resampler.
685		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		// The reset dropped everything, so the track ends without a packet.
694		assert!(track.read().await.unwrap().is_none());
695	}
696
697	/// Resetting after a full frame drops codec lookahead as well as producer buffers.
698	#[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	/// A codec reset starts a new pre-skip interval at the receiver too.
736	#[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	// One 20 ms Opus frame at 48 kHz mono is exactly 960 f32 samples, so each
836	// `write` of this drains precisely one packet (no resampler, no leftover).
837	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	/// Publish each frame and read back the resulting packet PTS (microseconds).
847	/// If `reset_before` contains an index, `reset_epoch()` is called before that
848	/// frame's `write`.
849	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		// Input rate == Opus codec rate, so there's no resampler and sample
855		// counts stay exact, making the PTS assertions deterministic.
856		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		// The first frame's timestamp becomes the epoch (regression guard: the
886		// old code derived PTS purely from the sample count, always near 0).
887		let pts = published_pts(&[full_frame(1_000_000)], None).await;
888		assert_eq!(pts, vec![1_000_000]);
889	}
890
891	/// The encoder needs no correction for the resampler's own delay: it anchors
892	/// the epoch to the first input timestamp and advances by emitted samples,
893	/// while `Resampler::process` drops its startup silence rather than passing it
894	/// on, so the first sample published is the first sample written. Anything
895	/// that let that delay through would shift every PTS on a resampled track,
896	/// which no `reset_epoch` would ever correct.
897	#[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		// 44.1 kHz in, and Opus only runs at 48 kHz, so this one resamples.
904		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		// A whole second goes out before the first read, so the default budget of zero
916		// would shed all but the last packet and the first PTS read would be the tail's.
917		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		// A second of audio, so the filter's delay is nowhere near the whole write.
927		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		// Second frame's own timestamp (way ahead) is ignored; PTS advances by
936		// exactly one 20 ms frame from the epoch.
937		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		// Frame at t=0, then reset_epoch + a frame at t=5s: the 5 s idle gap must
944		// appear in the PTS (otherwise audio drifts behind a wall-clock video track).
945		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	/// Finish leaves the handle, so abort can still run.
950	#[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	/// A sub-frame write never reaches the closed track, so the producer must
986	/// refuse it itself rather than buffering samples that cannot be published.
987	#[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	/// `Options::track = None` derives a codec-suffixed name rather than making
1013	/// the caller invent one, mirroring the video side. Pins the exact name the
1014	/// docs promise, and that a second producer doesn't collide with the first.
1015	#[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}