use std::collections::HashMap;
use std::task::Poll;
use std::time::Duration;
use bytes::Bytes;
use hang::catalog::{AudioCodec, Catalog, Container, VideoConfig};
use mp4_atom::DecodeMaybe;
use crate::Result;
use crate::catalog::Stream;
use crate::container::ExportSource;
use crate::container::Frame;
use crate::container::fmp4::Error;
use moq_net::Timestamp;
pub struct Export<S: Stream> {
source: crate::Source,
catalog: Option<S>,
latency: Duration,
fragment_duration: Option<Duration>,
tracks: HashMap<String, Fmp4Track>,
catalog_snapshot: Option<Catalog>,
init_emitted: bool,
}
#[derive(Clone, Debug)]
pub struct Fragment {
pub data: Bytes,
pub init: bool,
pub independent: bool,
pub duration: f64,
}
struct Fmp4Track {
source: ExportSource,
pending: Option<Frame>,
buffer: Vec<Frame>,
buffer_independent: bool,
is_video: bool,
opus: bool,
default_frame: Duration,
finished: bool,
track_id: u32,
timescale: u64,
sequence_number: u32,
}
impl<S: Stream> Export<S> {
pub fn new(source: crate::Source, catalog: S) -> Self {
Self {
source,
catalog: Some(catalog),
latency: Duration::ZERO,
fragment_duration: None,
tracks: HashMap::new(),
catalog_snapshot: None,
init_emitted: false,
}
}
pub fn with_latency(mut self, latency: Duration) -> Self {
self.latency = latency;
self
}
pub fn with_fragment_duration(mut self, duration: impl Into<Option<Duration>>) -> Self {
self.fragment_duration = duration.into();
self
}
pub async fn next(&mut self) -> Result<Option<Bytes>> {
Ok(self.next_fragment().await?.map(|f| f.data))
}
pub fn poll_next(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Bytes>>> {
Poll::Ready(Ok(std::task::ready!(self.poll_next_fragment(waiter)?).map(|f| f.data)))
}
pub async fn next_fragment(&mut self) -> Result<Option<Fragment>> {
kio::wait(|waiter| self.poll_next_fragment(waiter)).await
}
pub fn poll_next_fragment(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Fragment>>> {
while let Some(catalog) = self.catalog.as_mut() {
match catalog.poll_next(waiter)? {
Poll::Ready(Some(snapshot)) => self.update_catalog(&snapshot.media())?,
Poll::Ready(None) => {
self.catalog = None;
break;
}
Poll::Pending => break,
}
}
let waiting_for_init = !self.init_emitted;
for (name, track) in &mut self.tracks {
if track.pending.is_some() || track.finished {
continue;
}
loop {
match track.source.poll_read(waiter) {
Poll::Ready(Ok(Some(frame))) => {
let geometry_ready = !track.is_video
|| self
.catalog_snapshot
.as_ref()
.and_then(|catalog| catalog.video.renditions.get(name))
.is_some_and(|config| {
matches!(config.container, Container::Cmaf { .. })
|| track.source.video_geometry_ready(config)
});
if waiting_for_init && (!track.source.header_ready() || !geometry_ready) {
continue;
}
track.pending = Some(frame);
break;
}
Poll::Ready(Ok(None)) => {
track.finished = true;
break;
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => break,
}
}
}
if !self.init_emitted {
if self.init_ready() {
let init = self.build_init()?;
self.init_emitted = true;
return Poll::Ready(Ok(Some(Fragment {
data: init,
init: true,
independent: false,
duration: 0.0,
})));
}
if self.catalog.is_none() && self.tracks.values().all(|t| t.finished) {
return Poll::Ready(Ok(None));
}
return Poll::Pending;
}
let chosen = self
.tracks
.iter()
.filter_map(|(name, t)| t.pending.as_ref().map(|f| (name.clone(), f.timestamp)))
.min_by_key(|(_, ts)| *ts)
.map(|(name, _)| name);
if let Some(name) = chosen {
let frag = self.fragment_duration;
let has_video = self.tracks.values().any(|t| t.is_video);
let per_frame = frag == Some(Duration::ZERO) || (frag.is_none() && !has_video);
let track = self.tracks.get_mut(&name).unwrap();
let frame = track.pending.take().unwrap();
if per_frame {
if !track.buffer.is_empty() {
let frames = std::mem::take(&mut track.buffer);
let fragment = emit_fragment(track, frames, Some(&frame))?;
track.pending = Some(frame);
return Poll::Ready(Ok(Some(fragment)));
}
track.buffer_independent = frame.keyframe;
let fragment = emit_fragment(track, vec![frame], None)?;
return Poll::Ready(Ok(Some(fragment)));
}
if should_flush(track, &frame, frag) {
let frames = std::mem::take(&mut track.buffer);
let fragment = emit_fragment(track, frames, Some(&frame))?;
track.buffer_independent = frame.keyframe;
track.buffer.push(frame);
return Poll::Ready(Ok(Some(fragment)));
}
if track.buffer.is_empty() {
track.buffer_independent = frame.keyframe;
}
track.buffer.push(frame);
return self.poll_next_fragment(waiter);
}
let flushable = self
.tracks
.iter()
.filter_map(|(name, t)| {
if t.finished && !t.buffer.is_empty() {
Some((name.clone(), t.buffer.first().unwrap().timestamp))
} else {
None
}
})
.min_by_key(|(_, ts)| *ts)
.map(|(name, _)| name);
if let Some(name) = flushable {
let track = self.tracks.get_mut(&name).unwrap();
let frames = std::mem::take(&mut track.buffer);
let fragment = emit_fragment(track, frames, None)?;
return Poll::Ready(Ok(Some(fragment)));
}
if self.catalog.is_none() && self.tracks.values().all(|t| t.finished && t.buffer.is_empty()) {
return Poll::Ready(Ok(None));
}
self.tracks
.retain(|_, t| !(t.finished && t.pending.is_none() && t.buffer.is_empty()));
Poll::Pending
}
fn update_catalog(&mut self, catalog: &Catalog) -> Result<()> {
let mut catalog = catalog.clone();
catalog
.video
.renditions
.retain(|name, config| crate::catalog::hang::supported(name, &config.container));
catalog
.audio
.renditions
.retain(|name, config| crate::catalog::hang::supported(name, &config.container));
self.source.retain_valid_media(&mut catalog);
let catalog = &catalog;
let mut active: HashMap<String, ()> = HashMap::new();
for name in catalog.video.renditions.keys() {
active.insert(name.clone(), ());
}
for name in catalog.audio.renditions.keys() {
active.insert(name.clone(), ());
}
let mut next_track_id = self.tracks.values().map(|t| t.track_id).max().unwrap_or(0) + 1;
for (name, config) in &catalog.video.renditions {
if self.tracks.contains_key(name) {
continue;
}
let Some(source) = ExportSource::for_video(&self.source, name, config, self.latency)? else {
continue;
};
let timescale = catalog_timescale_video(config)?;
let framerate = super::usable_video_framerate(config).unwrap_or(30.0);
self.tracks.insert(
name.clone(),
Fmp4Track {
source,
pending: None,
buffer: Vec::new(),
buffer_independent: false,
is_video: true,
opus: false,
default_frame: Duration::from_secs_f64(1.0 / framerate),
finished: false,
track_id: next_track_id,
timescale,
sequence_number: 1,
},
);
next_track_id += 1;
}
for (name, config) in &catalog.audio.renditions {
if self.tracks.contains_key(name) {
continue;
}
let Some(source) = ExportSource::for_audio(&self.source, name, config, self.latency)? else {
continue;
};
let timescale = catalog_timescale_audio(config)?;
self.tracks.insert(
name.clone(),
Fmp4Track {
source,
pending: None,
buffer: Vec::new(),
buffer_independent: false,
is_video: false,
opus: matches!(config.codec, AudioCodec::Opus),
default_frame: Duration::from_secs_f64(1024.0 / config.sample_rate.max(1) as f64),
finished: false,
track_id: next_track_id,
timescale,
sequence_number: 1,
},
);
next_track_id += 1;
}
self.tracks.retain(|name, _| active.contains_key(name));
self.catalog_snapshot = Some(catalog.clone());
Ok(())
}
fn init_ready(&self) -> bool {
let Some(catalog) = self.catalog_snapshot.as_ref() else {
return false;
};
self.tracks.values().all(|t| t.source.header_ready())
&& catalog.video.renditions.iter().all(|(name, config)| {
matches!(config.container, Container::Cmaf { .. })
|| self
.tracks
.get(name)
.is_some_and(|track| track.source.video_geometry_ready(config))
})
}
fn build_init(&self) -> Result<Bytes> {
let catalog = self.catalog_snapshot.as_ref().ok_or(Error::NoCatalogSnapshot)?;
let mut traks: Vec<mp4_atom::Trak> = Vec::new();
let mut trexs: Vec<mp4_atom::Trex> = Vec::new();
let mut ftyp_data: Option<mp4_atom::Ftyp> = None;
for (name, config) in &catalog.video.renditions {
let track = self
.tracks
.get(name)
.ok_or_else(|| Error::MissingVideoTrack(name.clone()))?;
match &config.container {
Container::Cmaf { init, .. } => {
extract_init(init, track.track_id, &mut ftyp_data, &mut traks, &mut trexs)?;
}
Container::Legacy | Container::Loc => {
let description = track.source.description();
let config = track.source.video_config(config).unwrap_or_else(|| config.clone());
let trak = crate::container::fmp4::synthesize_video_trak(
track.track_id,
track.timescale,
&config,
description.map(|d| d.as_ref()),
)?;
trexs.push(mp4_atom::Trex {
track_id: trak.tkhd.track_id,
default_sample_description_index: 1,
..Default::default()
});
traks.push(trak);
}
Container::Unknown(unknown) => return Err(crate::Error::unsupported_container(unknown)),
}
}
for (name, config) in &catalog.audio.renditions {
let track = self
.tracks
.get(name)
.ok_or_else(|| Error::MissingAudioTrack(name.clone()))?;
match &config.container {
Container::Cmaf { init, .. } => {
extract_init(init, track.track_id, &mut ftyp_data, &mut traks, &mut trexs)?;
}
Container::Legacy | Container::Loc => {
let trak = crate::container::fmp4::synthesize_audio_trak(track.track_id, track.timescale, config)?;
trexs.push(mp4_atom::Trex {
track_id: trak.tkhd.track_id,
default_sample_description_index: 1,
..Default::default()
});
traks.push(trak);
}
Container::Unknown(unknown) => return Err(crate::Error::unsupported_container(unknown)),
}
}
Ok(crate::container::fmp4::encode_init(ftyp_data, traks, trexs)?)
}
}
pub(crate) fn extract_init(
init: &Bytes,
track_id: u32,
ftyp_data: &mut Option<mp4_atom::Ftyp>,
traks: &mut Vec<mp4_atom::Trak>,
trexs: &mut Vec<mp4_atom::Trex>,
) -> Result<()> {
let mut cursor = std::io::Cursor::new(init.as_ref());
while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
match atom {
mp4_atom::Any::Ftyp(f) if ftyp_data.is_none() => {
*ftyp_data = Some(f);
}
mp4_atom::Any::Moov(moov) => {
for mut trak in moov.trak {
trak.tkhd.track_id = track_id;
trak.edts = None;
trak.tkhd.duration = super::UNKNOWN_DURATION;
traks.push(trak);
}
if let Some(mvex) = moov.mvex {
for mut trex in mvex.trex {
trex.track_id = track_id;
trexs.push(trex);
}
}
}
_ => {}
}
}
Ok(())
}
fn should_flush(track: &Fmp4Track, frame: &Frame, fragment_duration: Option<Duration>) -> bool {
if track.buffer.is_empty() {
return false;
}
if track.is_video && frame.keyframe {
return true;
}
let Some(cap) = fragment_duration else {
return false;
};
let mut min = Duration::from(frame.timestamp);
let mut max = min;
for f in &track.buffer {
let pts = Duration::from(f.timestamp);
min = min.min(pts);
max = max.max(pts);
}
max.saturating_sub(min) >= cap
}
fn encode_fragment(track: &mut Fmp4Track, frames: Vec<Frame>) -> Result<Bytes> {
if frames.is_empty() {
return Err(Error::NoFrames.into());
}
let seq = track.sequence_number;
track.sequence_number += 1;
let timescale = moq_net::Timescale::new(track.timescale)?;
let info = crate::container::fmp4::FragmentInfo {
track_id: track.track_id,
timescale,
sequence_number: seq,
};
Ok(crate::container::fmp4::encode_fragment(info, &frames)?)
}
fn emit_fragment(track: &mut Fmp4Track, mut frames: Vec<Frame>, successor: Option<&Frame>) -> Result<Fragment> {
apply_codec_durations(&mut frames, track.opus);
let independent = !track.is_video || track.buffer_independent;
let timescale = moq_net::Timescale::new(track.timescale)?;
infer_missing_durations(&mut frames, successor, track.default_frame, timescale)?;
let duration = fragment_seconds(&frames, track.default_frame);
let data = encode_fragment(track, frames)?;
Ok(Fragment {
data,
init: false,
independent,
duration,
})
}
pub(crate) fn fragment_seconds(frames: &[Frame], default_frame: Duration) -> f64 {
if frames.is_empty() {
return 0.0;
}
if frames
.iter()
.all(|f| f.duration.is_some_and(|duration| !duration.is_zero()))
{
return frames
.iter()
.map(|f| Duration::from(f.duration.unwrap()))
.sum::<Duration>()
.as_secs_f64();
}
let mut min = Duration::MAX;
let mut max = Duration::ZERO;
for f in frames {
let pts = Duration::from(f.timestamp);
min = min.min(pts);
max = max.max(pts);
}
((max - min) + default_frame).as_secs_f64()
}
pub(crate) fn apply_codec_durations(frames: &mut [Frame], opus: bool) {
if !opus {
return;
}
for frame in frames {
if frame.duration.is_none() {
frame.duration = crate::codec::opus::packet_samples(&frame.payload)
.and_then(|samples| Timestamp::from_scale(samples as u64, 48_000).ok());
}
}
}
pub(crate) fn infer_missing_durations(
frames: &mut [Frame],
successor: Option<&Frame>,
default_frame: Duration,
timescale: moq_net::Timescale,
) -> Result<()> {
let infer_from_pts = pts_monotonic(frames, successor);
for i in 0..frames.len() {
let successor = infer_from_pts
.then(|| duration_bound(frames, successor, i))
.flatten()
.cloned();
infer_missing_duration(&mut frames[i], successor.as_ref(), default_frame, timescale)?;
}
Ok(())
}
pub(crate) fn infer_missing_duration(
frame: &mut Frame,
successor: Option<&Frame>,
default_frame: Duration,
timescale: moq_net::Timescale,
) -> Result<()> {
if frame.duration.is_some_and(|duration| !duration.is_zero()) {
return Ok(());
}
frame.duration = match successor {
Some(next) => timestamp_gap(frame.timestamp, next.timestamp, timescale)?,
None => None,
}
.or_else(|| fallback_duration(default_frame, timescale))
.filter(|duration| !duration.is_zero());
Ok(())
}
fn fallback_duration(default_frame: Duration, timescale: moq_net::Timescale) -> Option<Timestamp> {
if let Some(ticks) = super::rounded_duration_ticks(default_frame, timescale.as_u64()) {
Timestamp::new(ticks, timescale).ok()
} else {
Timestamp::try_from(default_frame).ok()
}
}
fn timestamp_gap(start: Timestamp, end: Timestamp, timescale: moq_net::Timescale) -> Result<Option<Timestamp>> {
let start_scale = u128::from(start.scale().as_u64());
let end_scale = u128::from(end.scale().as_u64());
let end_numerator = u128::from(end.value()) * start_scale;
let start_numerator = u128::from(start.value()) * end_scale;
let Some(numerator) = end_numerator.checked_sub(start_numerator) else {
return Ok(None);
};
if numerator == 0 {
return Ok(None);
}
let start_ticks = super::timestamp_ticks(start, timescale)?;
let end_ticks = super::timestamp_ticks(end, timescale)?;
let Some(ticks) = end_ticks.checked_sub(start_ticks) else {
return Ok(None);
};
if ticks == 0 {
return Err(Error::SampleDurationTooSmall(timescale.as_u64()).into());
}
Ok(Some(Timestamp::new(ticks, timescale)?))
}
fn pts_monotonic(frames: &[Frame], successor: Option<&Frame>) -> bool {
let frames_monotonic = frames.windows(2).all(|pair| pair[1].timestamp >= pair[0].timestamp);
let successor_monotonic = match (frames.last(), successor.filter(|next| !next.keyframe)) {
(Some(last), Some(successor)) => successor.timestamp >= last.timestamp,
_ => true,
};
frames_monotonic && successor_monotonic
}
fn duration_bound<'a>(frames: &'a [Frame], successor: Option<&'a Frame>, index: usize) -> Option<&'a Frame> {
frames.get(index + 1).or(successor).filter(|next| !next.keyframe)
}
pub(crate) fn catalog_timescale_video(config: &VideoConfig) -> Result<u64> {
Ok(match &config.container {
Container::Cmaf { init, .. } => {
parse_timescale_from_init(init).unwrap_or_else(|_| crate::container::fmp4::default_video_timescale(config))
}
Container::Loc | Container::Legacy => crate::container::fmp4::default_video_timescale(config),
Container::Unknown(unknown) => return Err(crate::Error::unsupported_container(unknown)),
})
}
pub(crate) fn catalog_timescale_audio(config: &hang::catalog::AudioConfig) -> Result<u64> {
Ok(match &config.container {
Container::Cmaf { init, .. } => parse_timescale_from_init(init).unwrap_or(config.sample_rate as u64),
Container::Loc | Container::Legacy => config.sample_rate as u64,
Container::Unknown(unknown) => return Err(crate::Error::unsupported_container(unknown)),
})
}
fn parse_timescale_from_init(init: &[u8]) -> Result<u64> {
let mut cursor = std::io::Cursor::new(init);
while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
if let mp4_atom::Any::Moov(moov) = atom {
let trak = moov.trak.first().ok_or(Error::NoTracks)?;
return Ok(trak.mdia.mdhd.timescale as u64);
}
}
Err(Error::NoMoov.into())
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use super::*;
fn ts(micros: u64) -> Timestamp {
Timestamp::from_micros(micros).unwrap()
}
fn duration_micros(frame: &Frame) -> u128 {
frame.duration.unwrap().as_micros()
}
fn frame(timestamp_us: u64, duration_us: Option<u64>) -> Frame {
Frame {
timestamp: ts(timestamp_us),
duration: duration_us.map(ts),
payload: Bytes::from_static(&[0xDE, 0xAD]),
keyframe: false,
}
}
fn group_start(timestamp_us: u64) -> Frame {
Frame {
keyframe: true,
..frame(timestamp_us, None)
}
}
#[test]
fn infer_missing_durations_uses_default_for_trailing_sample() {
let mut frames = vec![frame(0, Some(0)), frame(41_667, None), frame(83_334, None)];
infer_missing_durations(&mut frames, None, Duration::from_millis(33), moq_net::Timescale::MICRO).unwrap();
assert_eq!(frames[0].duration, Some(ts(41_667)));
assert_eq!(frames[1].duration, Some(ts(41_667)));
assert_eq!(duration_micros(&frames[2]), 33_000);
assert_eq!(fragment_seconds(&frames, Duration::from_millis(33)), 0.116334);
}
#[test]
fn infer_missing_duration_uses_default_for_single_frame() {
let mut frames = vec![frame(83_333, Some(0))];
infer_missing_durations(&mut frames, None, Duration::from_millis(40), moq_net::Timescale::MICRO).unwrap();
assert_eq!(duration_micros(&frames[0]), 40_000);
assert_eq!(fragment_seconds(&frames, Duration::from_millis(40)), 0.04);
}
#[test]
fn infer_trailing_duration_from_successor_frame() {
let successor = frame(83_334, None);
let mut frames = vec![frame(41_667, None)];
infer_missing_durations(
&mut frames,
Some(&successor),
Duration::from_millis(33),
moq_net::Timescale::MICRO,
)
.unwrap();
assert_eq!(frames[0].duration, Some(ts(41_667)));
assert_eq!(fragment_seconds(&frames, Duration::from_millis(33)), 0.041667);
}
#[test]
fn inferred_microsecond_clock_carries_fractional_ticks_forward() {
let timescale = moq_net::Timescale::new(30_000).unwrap();
let mut frames = vec![frame(0, None), frame(33_333, None), frame(66_667, None)];
infer_missing_durations(&mut frames, None, Duration::from_nanos(33_333_333), timescale).unwrap();
let durations: Vec<_> = frames
.iter()
.map(|frame| {
let duration = frame.duration.unwrap();
(duration.value(), duration.scale())
})
.collect();
assert_eq!(durations, vec![(1_000, timescale); 3]);
}
#[test]
fn infer_stops_at_a_group_boundary() {
let next_group = group_start(2_405_070_000);
let mut frames = vec![frame(63_244, None)];
infer_missing_durations(
&mut frames,
Some(&next_group),
Duration::from_millis(33),
moq_net::Timescale::MICRO,
)
.unwrap();
assert_eq!(duration_micros(&frames[0]), 33_000);
assert_eq!(fragment_seconds(&frames, Duration::from_millis(33)), 0.033);
}
#[test]
fn infer_stops_at_an_interior_group_boundary() {
let mut frames = vec![frame(0, None), frame(21_333, None), group_start(600_000_000)];
infer_missing_durations(&mut frames, None, Duration::from_millis(21), moq_net::Timescale::MICRO).unwrap();
assert_eq!(frames[0].duration, Some(ts(21_333)), "same group, real delta");
assert_eq!(duration_micros(&frames[1]), 21_000, "bounded by the next group");
assert_eq!(duration_micros(&frames[2]), 21_000, "nothing after it at all");
}
#[test]
fn infer_crosses_a_mid_group_fragment_boundary() {
let successor = frame(83_334, None);
let mut frames = vec![frame(41_667, None)];
infer_missing_durations(
&mut frames,
Some(&successor),
Duration::from_millis(33),
moq_net::Timescale::MICRO,
)
.unwrap();
assert_eq!(frames[0].duration, Some(ts(41_667)));
}
#[test]
fn codec_durations_keep_one_packet_groups_exact() {
let packet = Bytes::from_static(&[0x78, 0x00, 0x00, 0x00]);
let mut frames: Vec<Frame> = (0..3)
.map(|i| Frame {
payload: packet.clone(),
..group_start(i * 20_000)
})
.collect();
apply_codec_durations(&mut frames, true);
infer_missing_durations(
&mut frames,
None,
Duration::from_micros(21_333),
moq_net::Timescale::MICRO,
)
.unwrap();
for f in &frames {
assert_eq!(
f.duration.unwrap().as_micros(),
20_000,
"TOC duration, not the fallback"
);
}
}
#[test]
fn infer_ignores_a_rewound_group_boundary() {
let next_group = group_start(0);
let mut frames = vec![frame(1_000_000, None), frame(1_033_000, None)];
infer_missing_durations(
&mut frames,
Some(&next_group),
Duration::from_millis(50),
moq_net::Timescale::MICRO,
)
.unwrap();
assert_eq!(
frames[0].duration,
Some(ts(33_000)),
"in-group delta survives the rewind"
);
assert_eq!(duration_micros(&frames[1]), 50_000, "last in group falls back");
}
#[test]
fn infer_missing_durations_avoids_non_monotonic_pts() {
let successor = frame(66_000, None);
let mut frames = vec![frame(0, None), frame(99_000, None), frame(33_000, None)];
infer_missing_durations(
&mut frames,
Some(&successor),
Duration::from_millis(33),
moq_net::Timescale::MICRO,
)
.unwrap();
assert_eq!(duration_micros(&frames[0]), 33_000);
assert_eq!(duration_micros(&frames[1]), 33_000);
assert_eq!(duration_micros(&frames[2]), 33_000);
assert_eq!(fragment_seconds(&frames, Duration::from_millis(33)), 0.099);
}
#[test]
fn extract_init_strips_edit_lists() {
use mp4_atom::Encode;
let trak = mp4_atom::Trak {
edts: Some(mp4_atom::Edts {
elst: Some(mp4_atom::Elst {
entries: vec![mp4_atom::ElstEntry::default()],
}),
}),
..Default::default()
};
let moov = mp4_atom::Moov {
trak: vec![trak],
..Default::default()
};
let mut init = Vec::new();
moov.encode(&mut init).unwrap();
let mut traks = Vec::new();
let mut trexs = Vec::new();
let mut ftyp = None;
extract_init(&Bytes::from(init), 1, &mut ftyp, &mut traks, &mut trexs).unwrap();
assert_eq!(traks.len(), 1);
assert!(traks[0].edts.is_none(), "CMAF init must not carry an edit list");
}
#[test]
fn extract_init_clears_a_stale_track_duration() {
use mp4_atom::Encode;
let moov = mp4_atom::Moov {
mvhd: mp4_atom::Mvhd {
timescale: 1_000,
..Default::default()
},
trak: vec![mp4_atom::Trak {
tkhd: mp4_atom::Tkhd {
duration: 30_000, ..Default::default()
},
mdia: mp4_atom::Mdia {
mdhd: mp4_atom::Mdhd {
timescale: 48_000,
..Default::default()
},
..Default::default()
},
..Default::default()
}],
..Default::default()
};
let mut init = Vec::new();
moov.encode(&mut init).unwrap();
let mut traks = Vec::new();
let mut trexs = Vec::new();
let mut ftyp = None;
extract_init(&Bytes::from(init), 1, &mut ftyp, &mut traks, &mut trexs).unwrap();
assert_eq!(traks.len(), 1);
assert_eq!(
traks[0].tkhd.duration,
u64::MAX,
"a live fragmented init declares an unknown duration"
);
}
}