use std::time::Duration;
use bytes::Bytes;
use moq_mux::catalog::hang::CatalogExt;
use moq_mux::container::Frame as MuxFrame;
use moq_net::Timestamp;
use super::encoder::{Codec, Config, Encoder, Input};
use crate::resample::Resampler;
use crate::{Error, Frame};
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Options {
pub track: Option<String>,
pub codec: Codec,
pub sample_rate: Option<u32>,
pub channels: Option<u32>,
pub bitrate: Option<u32>,
pub frame_duration: Duration,
}
impl Default for Options {
fn default() -> Self {
Self {
track: None,
codec: Codec::default(),
sample_rate: None,
channels: None,
bitrate: None,
frame_duration: Duration::from_millis(20),
}
}
}
impl Options {
fn config(&self, input: Input) -> Config {
Config {
input,
codec: self.codec,
sample_rate: self.sample_rate,
channels: self.channels,
bitrate: self.bitrate,
frame_duration: self.frame_duration,
}
}
}
pub struct Producer<E: CatalogExt = ()> {
encoder: Encoder,
resampler: Option<Resampler>,
track: moq_mux::container::Producer<moq_mux::container::legacy::Wire>,
rendition: Rendition<E>,
pending: Vec<f32>,
frames_produced: u64,
epoch_us: Option<u64>,
}
impl<E: CatalogExt> Producer<E> {
pub fn new(
broadcast: &mut moq_net::broadcast::Producer,
catalog: moq_mux::catalog::Producer<E>,
input: Input,
options: &Options,
) -> Result<Self, Error> {
let encoder = Encoder::new(&options.config(input))?;
let input = &encoder.config().input;
let resampler = if input.sample_rate == encoder.codec_rate() {
None
} else {
let chunk_frames =
((input.sample_rate as u128 * encoder.config().frame_duration.as_micros()) / 1_000_000) as usize;
Some(Resampler::new(
input.sample_rate,
encoder.codec_rate(),
input.channels,
chunk_frames,
)?)
};
let track = match &options.track {
Some(name) => broadcast.create_track(name.clone(), hang::container::track_info())?,
None => moq_mux::import::unique_track(broadcast, &format!(".{}", options.codec))?,
};
let name = track.name().to_string();
let track = catalog.media_producer(track, moq_mux::container::legacy::Wire)?;
let mut catalog_mut = catalog.clone();
let mut config = encoder.catalog();
config.timeline = Some(catalog.timeline(&name)?.section());
catalog_mut.lock().audio.insert(&name, config)?;
Ok(Self {
encoder,
resampler,
track,
rendition: Rendition { catalog, name },
pending: Vec::new(),
frames_produced: 0,
epoch_us: None,
})
}
pub fn track_name(&self) -> &str {
&self.rendition.name
}
pub fn track(&self) -> &moq_net::track::Producer {
self.track.track()
}
pub fn reset_epoch(&mut self) {
self.epoch_us = None;
self.frames_produced = 0;
self.pending.clear();
}
pub fn write(&mut self, frame: &Frame) -> Result<(), Error> {
let timestamp_us = u64::try_from(frame.timestamp.as_micros())
.map_err(|_| Error::Unsupported(format!("frame timestamp {:?} out of range", frame.timestamp)))?;
let epoch_us = *self.epoch_us.get_or_insert(timestamp_us);
let input = &self.encoder.config().input;
let (format, channels) = (input.format, input.channels);
let pcm = format.as_interleaved_f32(frame.data.as_ref(), channels)?;
let pcm: Vec<f32> = match self.resampler.as_mut() {
Some(r) => r.process(&pcm)?,
None => pcm.into_owned(),
};
self.pending.extend(pcm);
let frame_samples = self.encoder.frame_size() * self.encoder.codec_channels() as usize;
while self.pending.len() >= frame_samples {
let chunk: Vec<f32> = self.pending.drain(..frame_samples).collect();
let packet = self.encoder.encode(&chunk)?;
let timestamp = self.timestamp(epoch_us)?;
self.frames_produced += self.encoder.frame_size() as u64;
self.publish(packet, timestamp)?;
}
Ok(())
}
fn timestamp(&self, epoch_us: u64) -> Result<Timestamp, Error> {
let offset_us = (self.frames_produced * 1_000_000) / self.encoder.codec_rate() as u64;
Ok(Timestamp::from_micros(epoch_us + offset_us)?)
}
fn publish(&mut self, payload: Bytes, timestamp: Timestamp) -> Result<(), Error> {
let mux_frame = MuxFrame {
timestamp,
payload,
keyframe: true,
duration: None,
};
self.track.write(mux_frame)?;
self.track.cut(None)?;
Ok(())
}
pub fn finish(mut self) -> Result<(), Error> {
let frame_samples = self.encoder.frame_size() * self.encoder.codec_channels() as usize;
if !self.pending.is_empty() {
self.pending.resize(frame_samples, 0.0);
let chunk = std::mem::take(&mut self.pending);
let packet = self.encoder.encode(&chunk)?;
let timestamp = self.timestamp(self.epoch_us.unwrap_or(0))?;
self.publish(packet, timestamp)?;
}
self.track.finish()?;
Ok(())
}
pub fn abort(self, err: moq_net::Error) {
self.track.abort(err);
}
}
struct Rendition<E: CatalogExt> {
catalog: moq_mux::catalog::Producer<E>,
name: String,
}
impl<E: CatalogExt> Drop for Rendition<E> {
fn drop(&mut self) {
self.catalog.lock().audio.remove(&self.name);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Format;
fn full_frame(timestamp_us: u64) -> Frame {
let mut data = Vec::with_capacity(960 * 4);
for _ in 0..960 {
data.extend_from_slice(&0.1f32.to_le_bytes());
}
Frame {
timestamp: Timestamp::from_micros(timestamp_us).unwrap(),
data: data.into(),
}
}
async fn published_pts(frames: &[Frame], reset_before: Option<usize>) -> Vec<u128> {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
let consumer = broadcast.consume();
let input = Input {
format: Format::F32,
sample_rate: 48_000,
channels: 1,
};
let options = Options {
track: Some("audio".to_string()),
..Options::default()
};
let mut producer = Producer::new(&mut broadcast, catalog, input, &options).unwrap();
let track = consumer.track("audio").unwrap().subscribe(None).await.unwrap();
let mut reader = moq_mux::container::Consumer::new(track, moq_mux::container::legacy::Wire);
let mut pts = Vec::new();
for (i, frame) in frames.iter().enumerate() {
if reset_before == Some(i) {
producer.reset_epoch();
}
producer.write(frame).unwrap();
let read = reader.read().await.unwrap().expect("a packet per full frame");
pts.push(read.timestamp.as_micros());
}
pts
}
#[tokio::test]
async fn epoch_anchors_to_first_frame_timestamp() {
let pts = published_pts(&[full_frame(1_000_000)], None).await;
assert_eq!(pts, vec![1_000_000]);
}
#[tokio::test]
async fn pts_advances_by_frame_duration_ignoring_later_timestamps() {
let pts = published_pts(&[full_frame(1_000), full_frame(999_999)], None).await;
assert_eq!(pts, vec![1_000, 1_000 + 20_000]);
}
#[tokio::test]
async fn reset_epoch_reanchors_so_the_gap_lands_in_pts() {
let pts = published_pts(&[full_frame(0), full_frame(5_000_000)], Some(1)).await;
assert_eq!(pts, vec![0, 5_000_000]);
}
#[tokio::test]
async fn default_options_derive_the_track_name() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
let first = Producer::new(&mut broadcast, catalog.clone(), Input::default(), &Options::default()).unwrap();
assert_eq!(first.track_name(), "0.opus");
let second = Producer::new(&mut broadcast, catalog, Input::default(), &Options::default()).unwrap();
assert_eq!(second.track_name(), "1.opus");
}
}