use std::time::Duration;
use bytes::Bytes;
use hang::catalog::{AudioConfig, Container as CatalogContainer, VideoConfig};
use mp4_atom::Encode;
use crate::catalog::hang::Container as HangContainer;
use crate::container::Frame;
use crate::container::source::{VideoTransform, build_video_transform};
use super::export::{catalog_timescale_audio, catalog_timescale_video, extract_init, infer_missing_durations};
use super::{Error, synthesize_audio_trak, synthesize_video_trak};
const TRACK_ID: u32 = 1;
enum Kind {
Video(VideoConfig),
Audio(AudioConfig),
}
pub struct Muxer {
kind: Kind,
container: HangContainer,
transform: Option<VideoTransform>,
description: Option<Bytes>,
timescale: u64,
default_frame: Duration,
}
impl Muxer {
pub fn video(config: &VideoConfig) -> crate::Result<Self> {
let container = (&config.container).try_into()?;
let framerate = config
.framerate
.filter(|fps| fps.is_finite() && *fps > 0.0)
.unwrap_or(30.0);
Ok(Self {
container,
transform: build_video_transform(config),
description: config.description.as_ref().filter(|b| !b.is_empty()).cloned(),
timescale: catalog_timescale_video(config)?,
default_frame: Duration::from_secs_f64(1.0 / framerate),
kind: Kind::Video(config.clone()),
})
}
pub fn audio(config: &AudioConfig) -> crate::Result<Self> {
let container = (&config.container).try_into()?;
Ok(Self {
container,
transform: None,
description: config.description.as_ref().filter(|b| !b.is_empty()).cloned(),
timescale: catalog_timescale_audio(config)?,
default_frame: Duration::from_secs_f64(1024.0 / config.sample_rate.max(1) as f64),
kind: Kind::Audio(config.clone()),
})
}
pub async fn read(&mut self, group: &mut moq_net::group::Consumer) -> crate::Result<Vec<Frame>> {
use crate::container::Container as _;
let mut out: Vec<Frame> = Vec::new();
while let Some(frames) = self.container.read(group).await? {
for frame in frames {
let Some(transform) = self.transform.as_mut() else {
out.push(frame);
continue;
};
let payload = transform.transform(frame.payload.clone())?;
if let Some(d) = transform.codec_private()
&& self.description.as_ref() != Some(d)
{
self.description = Some(d.clone());
}
if let Some(payload) = payload {
out.push(Frame { payload, ..frame });
}
}
}
if let Some(first) = out.first_mut() {
first.keyframe = true;
}
Ok(out)
}
pub fn init(&self) -> crate::Result<Option<Bytes>> {
if self.transform.is_some() && self.description.is_none() {
return Ok(None);
}
let mut traks: Vec<mp4_atom::Trak> = Vec::new();
let mut trexs: Vec<mp4_atom::Trex> = Vec::new();
let mut ftyp: Option<mp4_atom::Ftyp> = None;
let container = match &self.kind {
Kind::Video(config) => &config.container,
Kind::Audio(config) => &config.container,
};
match container {
CatalogContainer::Cmaf { init, .. } => {
extract_init(init, TRACK_ID, &mut ftyp, &mut traks, &mut trexs)?;
}
CatalogContainer::Legacy | CatalogContainer::Loc => {
let trak = match &self.kind {
Kind::Video(config) => {
synthesize_video_trak(TRACK_ID, self.timescale, config, self.description.as_deref())?
}
Kind::Audio(config) => synthesize_audio_trak(TRACK_ID, self.timescale, config)?,
};
trexs.push(mp4_atom::Trex {
track_id: trak.tkhd.track_id,
default_sample_description_index: 1,
..Default::default()
});
traks.push(trak);
}
CatalogContainer::Unknown(unknown) => return Err(crate::Error::unsupported_container(unknown)),
}
let ftyp = ftyp.unwrap_or(mp4_atom::Ftyp {
major_brand: b"isom".into(),
minor_version: 0x200,
compatible_brands: vec![b"isom".into(), b"iso6".into(), b"mp41".into()],
});
let timescale = traks.first().map(|t| t.mdia.mdhd.timescale).unwrap_or(1000);
let moov = mp4_atom::Moov {
mvhd: mp4_atom::Mvhd {
timescale,
..Default::default()
},
trak: traks,
mvex: if trexs.is_empty() {
None
} else {
Some(mp4_atom::Mvex {
trex: trexs,
..Default::default()
})
},
..Default::default()
};
let mut buf = Vec::new();
ftyp.encode(&mut buf).map_err(Error::from)?;
moov.encode(&mut buf).map_err(Error::from)?;
Ok(Some(Bytes::from(buf)))
}
pub fn fragment(&self, sequence: u32, frames: &[Frame]) -> crate::Result<Bytes> {
let frames = infer_missing_durations(frames.to_vec(), None, self.default_frame);
let timescale = moq_net::Timescale::new(self.timescale).map_err(Error::from)?;
Ok(super::encode_fragment(TRACK_ID, timescale, sequence, &frames)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use hang::catalog::VideoCodec;
use moq_net::Timestamp;
fn frame(micros: u64, keyframe: bool) -> Frame {
Frame {
timestamp: Timestamp::from_micros(micros).unwrap(),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe,
duration: None,
}
}
#[tokio::test]
async fn legacy_group_round_trips() {
let track = moq_net::broadcast::Info::new()
.produce()
.create_track("v", None)
.unwrap();
let mut subscriber = track.subscribe(None);
let mut producer = crate::container::Producer::new(track, HangContainer::Legacy);
producer.write(frame(10_000_000, true)).unwrap();
producer.write(frame(10_033_000, false)).unwrap();
producer.finish().unwrap();
let mut group = subscriber.next_group().await.unwrap().expect("a group");
let mut config = VideoConfig::new(VideoCodec::VP8);
config.framerate = Some(30.0);
let mut muxer = Muxer::video(&config).unwrap();
let init = muxer.init().unwrap().expect("init buildable for an out-of-band codec");
assert_eq!(&init[4..8], b"ftyp");
let frames = muxer.read(&mut group).await.unwrap();
assert_eq!(frames.len(), 2);
assert!(frames[0].keyframe, "the group's first frame is a keyframe");
let fragment = muxer.fragment(7, &frames).unwrap();
assert_eq!(&fragment[4..8], b"moof");
let timescale = moq_net::Timescale::new(30_000).unwrap();
let decoded = super::super::decode(fragment, timescale).unwrap();
assert_eq!(decoded.len(), 2);
assert_eq!(decoded[0].timestamp.as_micros(), 10_000_000);
assert!(decoded[0].keyframe);
assert_eq!(decoded[1].timestamp.as_micros(), 10_033_000);
}
#[tokio::test]
async fn audio_fragment_infers_durations_from_successors() {
use hang::catalog::AudioCodec;
let config = AudioConfig::new(AudioCodec::Opus, 48_000, 2);
let muxer = Muxer::audio(&config).unwrap();
let frames: Vec<Frame> = (0..4).map(|i| frame(i * 20_000, true)).collect();
let fragment = muxer.fragment(0, &frames).unwrap();
let timescale = moq_net::Timescale::new(48_000).unwrap();
let decoded = super::super::decode(fragment, timescale).unwrap();
assert_eq!(decoded.len(), 4);
for f in &decoded[..3] {
assert_eq!(f.duration.unwrap().as_micros(), 20_000, "successor-derived duration");
}
let last = decoded[3].duration.unwrap().as_micros();
assert!(
(21_000..21_400).contains(&last),
"last sample uses the codec fallback, got {last}"
);
}
}