use std::time::Duration;
use bytes::Bytes;
use hang::catalog::{AudioConfig, Container as CatalogContainer, VideoConfig};
use crate::catalog::hang::Container as HangContainer;
use crate::container::Frame;
use crate::container::source::{VideoTransform, build_video_transform};
use super::export::{
apply_codec_durations, catalog_timescale_audio, catalog_timescale_video, extract_init, infer_missing_durations,
};
use super::{Error, Fragmenter, fragment, 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: moq_net::Timescale,
default_frame: Duration,
opus: bool,
}
impl Muxer {
pub fn video(config: &VideoConfig) -> crate::Result<Self> {
let container = (&config.container).try_into()?;
let framerate = super::usable_video_framerate(config).unwrap_or(30.0);
Ok(Self {
container,
transform: build_video_transform(config),
description: config.description.as_ref().filter(|b| !b.is_empty()).cloned(),
timescale: moq_net::Timescale::new(catalog_timescale_video(config)?).map_err(Error::from)?,
default_frame: Duration::from_secs_f64(1.0 / framerate),
opus: false,
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: moq_net::Timescale::new(catalog_timescale_audio(config)?).map_err(Error::from)?,
default_frame: Duration::from_secs_f64(1024.0 / config.sample_rate.max(1) as f64),
opus: matches!(config.codec, hang::catalog::AudioCodec::Opus),
kind: Kind::Audio(config.clone()),
})
}
pub fn timescale(&self) -> moq_net::Timescale {
self.timescale
}
pub fn with_timescale(mut self, timescale: moq_net::Timescale) -> crate::Result<Self> {
if matches!(self.catalog_container(), CatalogContainer::Cmaf { .. }) {
return Err(Error::TimescaleOverride.into());
}
super::mdhd_timescale(timescale.as_u64())?;
self.timescale = timescale;
Ok(self)
}
fn catalog_container(&self) -> &CatalogContainer {
match &self.kind {
Kind::Video(config) => &config.container,
Kind::Audio(config) => &config.container,
}
}
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;
match self.catalog_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.as_u64(), config, self.description.as_deref())?
}
Kind::Audio(config) => synthesize_audio_trak(TRACK_ID, self.timescale.as_u64(), 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)),
}
Ok(Some(super::encode_init(ftyp, traks, trexs)?))
}
pub fn fragment(&self, sequence: u32, frames: &[Frame]) -> crate::Result<Bytes> {
let frames = self.resolve_durations(frames)?;
Ok(super::encode_fragment(self.fragment_info(sequence), &frames)?)
}
pub fn fragmenter(&self, config: fragment::Config) -> Fragmenter {
let is_video = matches!(self.kind, Kind::Video(_));
Fragmenter {
track_id: TRACK_ID,
timescale: self.timescale,
default_frame: self.default_frame,
is_video,
opus: self.opus,
infer_missing: !is_video
|| matches!(
config.missing_duration,
fragment::MissingDuration::InferFromPresentationTime
),
pending: None,
dts: None,
sequence: 0,
}
}
fn resolve_durations(&self, frames: &[Frame]) -> crate::Result<Vec<Frame>> {
let mut frames = frames.to_vec();
apply_codec_durations(&mut frames, self.opus);
infer_missing_durations(&mut frames, None, self.default_frame, self.timescale)?;
Ok(frames)
}
fn fragment_info(&self, sequence: u32) -> super::FragmentInfo {
super::FragmentInfo {
track_id: TRACK_ID,
timescale: self.timescale,
sequence_number: sequence,
}
}
}
#[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 muxer = video_muxer();
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);
}
fn video_muxer() -> Muxer {
let mut config = VideoConfig::new(VideoCodec::VP8);
config.framerate = Some(30.0);
Muxer::video(&config).unwrap()
}
#[test]
fn fragment_with_no_frames_is_empty() {
assert!(video_muxer().fragment(0, &[]).unwrap().is_empty());
}
#[test]
fn ntsc_fallback_duration_uses_the_derived_timescale() {
let mut config = VideoConfig::new(VideoCodec::VP8);
config.framerate = Some(30_000.0 / 1001.0);
let muxer = Muxer::video(&config).unwrap();
assert_eq!(muxer.timescale().as_u64(), 30_000);
let frame = Frame {
timestamp: Timestamp::ZERO,
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: true,
duration: None,
};
let fragment = muxer.fragment(0, std::slice::from_ref(&frame)).unwrap();
assert_eq!(super::super::sample_durations(&fragment), vec![Some(1001)]);
let mut fragmenter = muxer.fragmenter(fragment::Config {
missing_duration: fragment::MissingDuration::InferFromPresentationTime,
});
assert!(fragmenter.push(frame).unwrap().is_empty());
let fragment = fragmenter.flush().unwrap().unwrap();
assert_eq!(super::super::sample_durations(&fragment.data), vec![Some(1001)]);
}
#[test]
fn microsecond_pts_quantize_without_timeline_drift() {
let muxer = video_muxer();
let input = [0, 33_333, 66_667].map(|micros| Frame {
timestamp: Timestamp::from_micros(micros).unwrap(),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: micros == 0,
duration: None,
});
let fragment = muxer.fragment(0, &input).unwrap();
assert_eq!(super::super::sample_durations(&fragment), vec![Some(1_000); 3]);
assert_eq!(super::super::timeline(&fragment), (0, vec![0; 3]));
let mut fragmenter = muxer.fragmenter(fragment::Config {
missing_duration: fragment::MissingDuration::InferFromPresentationTime,
});
let mut fragments = Vec::new();
for frame in input {
fragments.extend(fragmenter.push(frame).unwrap());
}
fragments.extend(fragmenter.flush().unwrap());
let durations: Vec<_> = fragments
.iter()
.map(|fragment| super::super::sample_durations(&fragment.data)[0])
.collect();
assert_eq!(durations, vec![Some(1_000); 3]);
let timelines: Vec<_> = fragments
.iter()
.map(|fragment| super::super::timeline(&fragment.data))
.collect();
assert_eq!(timelines, vec![(0, vec![0]), (1_000, vec![0]), (2_000, vec![0])]);
}
#[test]
fn low_framerate_fallback_fits_mp4_timing_fields() {
let mut config = VideoConfig::new(VideoCodec::VP8);
config.framerate = Some(0.0011);
let muxer = Muxer::video(&config).unwrap();
assert_eq!(muxer.timescale().as_u64(), 11);
assert!(muxer.init().unwrap().is_some());
let frame = Frame {
timestamp: Timestamp::ZERO,
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: true,
duration: None,
};
let fragment = muxer.fragment(0, &[frame]).unwrap();
assert_eq!(super::super::sample_durations(&fragment), vec![Some(10_000)]);
}
#[test]
fn unusable_framerate_uses_the_standard_fallback_rate() {
let mut config = VideoConfig::new(VideoCodec::VP8);
config.framerate = Some(0.0005);
let muxer = Muxer::video(&config).unwrap();
let timescale = moq_net::Timescale::new(90_000).unwrap();
assert_eq!(muxer.timescale(), timescale);
let frame = Frame {
timestamp: Timestamp::ZERO,
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: true,
duration: None,
};
let decoded = super::super::decode(muxer.fragment(0, &[frame]).unwrap(), timescale).unwrap();
assert_eq!(decoded[0].duration.unwrap().as_scale(timescale), 3_000);
}
#[test]
fn with_timescale_overrides_the_catalog_derived_scale() {
let timescale = moq_net::Timescale::new(90_000).unwrap();
assert_eq!(video_muxer().timescale().as_u64(), 30_000, "framerate * 1000");
let muxer = video_muxer().with_timescale(timescale).unwrap();
assert_eq!(muxer.timescale(), timescale);
let init = muxer.init().unwrap().expect("init buildable for an out-of-band codec");
let trak = super::super::Wire::from_init(&init).unwrap();
assert_eq!(trak.trak().mdia.mdhd.timescale, 90_000);
let frame = Frame {
timestamp: Timestamp::from_scale(3_000, 90_000).unwrap(),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: true,
duration: Some(Timestamp::from_scale(3_000, 90_000).unwrap()),
};
let decoded = super::super::decode(muxer.fragment(0, &[frame]).unwrap(), timescale).unwrap();
assert_eq!(decoded[0].timestamp.as_micros(), 33_333);
}
#[test]
fn with_timescale_recomputes_the_fallback_frame_duration() {
let timescale = moq_net::Timescale::new(90_000).unwrap();
let muxer = video_muxer().with_timescale(timescale).unwrap();
let frame = Frame {
timestamp: Timestamp::ZERO,
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: true,
duration: None,
};
let decoded = super::super::decode(muxer.fragment(0, &[frame]).unwrap(), timescale).unwrap();
assert_eq!(decoded[0].duration.unwrap().as_scale(timescale), 3_000);
}
#[test]
fn with_timescale_rejects_a_scale_too_large_for_mdhd() {
let too_large = moq_net::Timescale::new(u64::from(u32::MAX) + 1).unwrap();
assert!(matches!(
video_muxer().with_timescale(too_large),
Err(crate::Error::Cmaf(Error::TimescaleTooLarge(_)))
));
let largest = moq_net::Timescale::new(u64::from(u32::MAX)).unwrap();
let muxer = video_muxer().with_timescale(largest).unwrap();
assert_eq!(muxer.timescale(), largest);
}
#[test]
fn init_rejects_a_catalog_scale_too_large_for_mdhd() {
let mut config = VideoConfig::new(VideoCodec::VP8);
config.framerate = Some(5_000_000.0); let err = Muxer::video(&config).unwrap().init().unwrap_err();
assert!(
matches!(err, crate::Error::Cmaf(Error::TimescaleTooLarge(_))),
"got {err:?}"
);
}
#[test]
fn with_timescale_rejects_a_cmaf_rendition() {
let init = video_muxer()
.with_timescale(moq_net::Timescale::new(48_000).unwrap())
.unwrap()
.init()
.unwrap()
.unwrap();
let mut config = VideoConfig::new(VideoCodec::VP8);
config.framerate = Some(30.0);
config.container = CatalogContainer::Cmaf { init };
let muxer = Muxer::video(&config).unwrap();
assert_eq!(muxer.timescale().as_u64(), 48_000, "read from the init segment");
assert!(muxer.with_timescale(moq_net::Timescale::new(90_000).unwrap()).is_err());
}
#[tokio::test]
async fn audio_fragment_takes_durations_from_the_codec() {
use hang::catalog::AudioCodec;
let config = AudioConfig::new(AudioCodec::Opus, 48_000, 2);
let muxer = Muxer::audio(&config).unwrap();
let packet = Bytes::from_static(&[0x78, 0x00, 0x00, 0x00]);
let frames: Vec<Frame> = (0..4)
.map(|i| Frame {
payload: packet.clone(),
..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 {
assert_eq!(
f.duration.unwrap().as_micros(),
20_000,
"TOC duration, not the fallback"
);
}
}
#[tokio::test]
async fn audio_fragment_does_not_absorb_a_pause() {
use hang::catalog::AudioCodec;
let config = AudioConfig::new(AudioCodec::Opus, 48_000, 2);
let muxer = Muxer::audio(&config).unwrap();
let packet = Bytes::from_static(&[0x78, 0x00, 0x00, 0x00]);
let frames: Vec<Frame> = [63_244, 2_405_070_000]
.into_iter()
.map(|micros| Frame {
payload: packet.clone(),
..frame(micros, 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();
let first = decoded[0].duration.unwrap().as_micros();
assert_eq!(first, 20_000, "the pause is a discontinuity, not a 2405 second sample");
}
}