Skip to main content

moq_ffi/
audio.rs

1//! Raw-audio import/export via [`moq_audio`].
2//!
3//! Sibling to [`producer::MoqMediaProducer`](crate::producer::MoqMediaProducer)
4//! and [`consumer::MoqMediaConsumer`](crate::consumer::MoqMediaConsumer):
5//! those deal in already-encoded frames, these deal in PCM and run
6//! Opus encode/decode inside the FFI boundary.
7
8use std::sync::Arc;
9use std::time::Duration;
10
11use crate::consumer::MoqBroadcastConsumer;
12use crate::error::MoqError;
13use crate::ffi::Task;
14use crate::producer::MoqBroadcastProducer;
15
16/// Raw PCM sample format, mirroring WebCodecs `AudioData.format`.
17///
18/// <https://developer.mozilla.org/en-US/docs/Web/API/AudioData/format>
19#[derive(Clone, Copy, uniffi::Enum)]
20pub enum MoqAudioFormat {
21	U8,
22	S16,
23	S32,
24	F32,
25	U8Planar,
26	S16Planar,
27	S32Planar,
28	F32Planar,
29}
30
31impl From<MoqAudioFormat> for moq_audio::AudioFormat {
32	fn from(f: MoqAudioFormat) -> Self {
33		match f {
34			MoqAudioFormat::U8 => Self::U8,
35			MoqAudioFormat::S16 => Self::S16,
36			MoqAudioFormat::S32 => Self::S32,
37			MoqAudioFormat::F32 => Self::F32,
38			MoqAudioFormat::U8Planar => Self::U8Planar,
39			MoqAudioFormat::S16Planar => Self::S16Planar,
40			MoqAudioFormat::S32Planar => Self::S32Planar,
41			MoqAudioFormat::F32Planar => Self::F32Planar,
42		}
43	}
44}
45
46/// Audio codec identifier.
47#[derive(Clone, Copy, uniffi::Enum)]
48pub enum MoqAudioCodec {
49	Opus,
50}
51
52impl From<MoqAudioCodec> for moq_audio::Codec {
53	fn from(c: MoqAudioCodec) -> Self {
54		match c {
55			MoqAudioCodec::Opus => Self::Opus,
56		}
57	}
58}
59
60/// PCM layout the caller will pass to [`MoqAudioProducer::write`].
61#[derive(uniffi::Record)]
62pub struct MoqAudioEncoderInput {
63	pub format: MoqAudioFormat,
64	pub sample_rate: u32,
65	pub channels: u32,
66}
67
68/// Codec-side configuration. `sample_rate` / `channels` `None` means
69/// "match the input (snapping the rate up to a libopus-supported
70/// value if necessary)".
71#[derive(uniffi::Record)]
72pub struct MoqAudioEncoderOutput {
73	pub codec: MoqAudioCodec,
74	pub sample_rate: Option<u32>,
75	pub channels: Option<u32>,
76	pub bitrate: Option<u32>,
77	/// Encoded frame duration in milliseconds. Opus accepts
78	/// 2.5/5/10/20/40/60 ms; pass 20 to match the JS publish path.
79	pub frame_duration_ms: u32,
80}
81
82/// PCM layout the caller wants out of [`MoqAudioConsumer::next`].
83#[derive(uniffi::Record)]
84pub struct MoqAudioDecoderOutput {
85	pub format: MoqAudioFormat,
86	/// `None` delivers samples at the codec's native rate.
87	pub sample_rate: Option<u32>,
88	/// `None` delivers samples at the codec's native channel count.
89	pub channels: Option<u32>,
90	/// Upper bound on buffering before skipping a stalled group, in
91	/// milliseconds. Same congestion-control knob as
92	/// [`MoqBroadcastConsumer::subscribe_media`](crate::consumer::MoqBroadcastConsumer::subscribe_media)'s
93	/// `max_latency_ms`: when a group stalls and a newer group is more
94	/// than this far ahead, the consumer skips. `None` keeps the
95	/// moq-mux default of zero (skip aggressively). Named `_max` to
96	/// leave room for a future `latency_min_ms` (jitter buffer).
97	pub latency_max_ms: Option<u64>,
98}
99
100/// One audio frame: payload bytes plus a presentation timestamp.
101///
102/// PCM layout is fixed by the producer / consumer config, so it is
103/// **not** carried per-frame. On the producer side `data` is raw PCM
104/// in the configured `input_format`; on the consumer side it is raw
105/// PCM in the configured `output_format`.
106#[derive(uniffi::Record)]
107pub struct MoqAudioFrame {
108	pub timestamp_us: u64,
109	pub data: Vec<u8>,
110}
111
112impl From<moq_audio::Frame> for MoqAudioFrame {
113	fn from(f: moq_audio::Frame) -> Self {
114		Self {
115			timestamp_us: f.timestamp_us,
116			data: f.data.to_vec(),
117		}
118	}
119}
120
121impl From<MoqAudioFrame> for moq_audio::Frame {
122	fn from(f: MoqAudioFrame) -> Self {
123		Self {
124			timestamp_us: f.timestamp_us,
125			data: f.data.into(),
126		}
127	}
128}
129
130// ---- Producer ----
131
132/// Producer for a raw-audio track.
133///
134/// Built via [`MoqBroadcastProducer::publish_audio`]. Each
135/// [`write`](Self::write) accepts an [`MoqAudioFrame`] whose `data`
136/// is PCM in the format declared by the [`MoqAudioEncoderInput`]
137/// passed at publish time.
138#[derive(uniffi::Object)]
139pub struct MoqAudioProducer {
140	inner: std::sync::Mutex<Option<moq_audio::AudioProducer>>,
141}
142
143#[uniffi::export]
144impl MoqAudioProducer {
145	pub fn write(&self, frame: MoqAudioFrame) -> Result<(), MoqError> {
146		let _guard = crate::ffi::RUNTIME.enter();
147		let mut guard = self.inner.lock().unwrap();
148		let producer = guard.as_mut().ok_or(MoqError::Closed)?;
149		producer.write(&frame.into())?;
150		Ok(())
151	}
152
153	pub fn finish(&self) -> Result<(), MoqError> {
154		let _guard = crate::ffi::RUNTIME.enter();
155		let producer = self.inner.lock().unwrap().take().ok_or(MoqError::Closed)?;
156		producer.finish()?;
157		Ok(())
158	}
159}
160
161#[uniffi::export]
162impl MoqBroadcastProducer {
163	/// Open an audio track on this broadcast. The catalog rendition is
164	/// registered immediately so subscribers can find the track even
165	/// before the first frame is written.
166	pub fn publish_audio(
167		&self,
168		name: String,
169		input: MoqAudioEncoderInput,
170		output: MoqAudioEncoderOutput,
171	) -> Result<Arc<MoqAudioProducer>, MoqError> {
172		let _guard = crate::ffi::RUNTIME.enter();
173
174		let producer = self.with_state(|state| {
175			moq_audio::AudioProducer::new(
176				&mut state.broadcast,
177				state.catalog.clone(),
178				name,
179				moq_audio::EncoderInput {
180					format: input.format.into(),
181					sample_rate: input.sample_rate,
182					channels: input.channels,
183				},
184				moq_audio::EncoderOutput {
185					codec: output.codec.into(),
186					sample_rate: output.sample_rate,
187					channels: output.channels,
188					bitrate: output.bitrate,
189					frame_duration: Duration::from_millis(output.frame_duration_ms.into()),
190				},
191			)
192			.map_err(Into::into)
193		})?;
194
195		Ok(Arc::new(MoqAudioProducer {
196			inner: std::sync::Mutex::new(Some(producer)),
197		}))
198	}
199}
200
201// ---- Consumer ----
202
203struct ConsumerInner {
204	consumer: moq_audio::AudioConsumer,
205}
206
207impl ConsumerInner {
208	async fn next(&mut self) -> Result<Option<MoqAudioFrame>, MoqError> {
209		Ok(self.consumer.read().await?.map(Into::into))
210	}
211}
212
213/// Consumer for a raw-audio track.
214#[derive(uniffi::Object)]
215pub struct MoqAudioConsumer {
216	task: Task<ConsumerInner>,
217}
218
219#[uniffi::export]
220impl MoqAudioConsumer {
221	pub async fn next(&self) -> Result<Option<MoqAudioFrame>, MoqError> {
222		self.task.run(|mut state| async move { state.next().await }).await
223	}
224
225	pub fn cancel(&self) {
226		self.task.cancel();
227	}
228}
229
230#[uniffi::export]
231impl MoqBroadcastConsumer {
232	/// Subscribe to an audio track. `catalog_audio_config` comes from
233	/// the catalog (see
234	/// [`MoqCatalogConsumer::next`](crate::consumer::MoqCatalogConsumer::next));
235	/// the codec is inferred from it.
236	pub fn subscribe_audio(
237		&self,
238		name: String,
239		catalog_audio: crate::media::MoqAudio,
240		output: MoqAudioDecoderOutput,
241	) -> Result<Arc<MoqAudioConsumer>, MoqError> {
242		let _guard = crate::ffi::RUNTIME.enter();
243
244		let mut cfg = hang::catalog::AudioConfig::new(
245			hang::catalog::AudioCodec::Opus,
246			catalog_audio.sample_rate,
247			catalog_audio.channel_count,
248		);
249		cfg.bitrate = catalog_audio.bitrate;
250		cfg.description = catalog_audio.description.map(Into::into);
251		cfg.container = catalog_audio.container.into();
252
253		let consumer = moq_audio::AudioConsumer::new(
254			self.inner(),
255			&cfg,
256			name,
257			moq_audio::DecoderOutput {
258				format: output.format.into(),
259				sample_rate: output.sample_rate,
260				channels: output.channels,
261				latency_max: output.latency_max_ms.map(Duration::from_millis),
262			},
263		)?;
264
265		Ok(Arc::new(MoqAudioConsumer {
266			task: Task::new(ConsumerInner { consumer }),
267		}))
268	}
269}