1mod export;
10pub mod fragment;
11mod fragmenter;
12mod import;
13mod muxer;
14
15pub use export::*;
16pub use fragmenter::*;
17pub use import::*;
18pub use muxer::*;
19
20pub use mp4_atom;
23
24#[cfg(test)]
25mod export_test;
26#[cfg(test)]
27mod import_test;
28
29use std::{task::Poll, time::Duration};
30
31use bytes::Bytes;
32use hang::catalog::{AudioCodec, AudioConfig, VideoCodec, VideoConfig};
33use mp4_atom::Atom;
34
35use moq_net::Timestamp;
36
37use crate::container::{Container, Frame};
38
39#[derive(Debug, Clone, thiserror::Error)]
40#[non_exhaustive]
41pub enum Error {
42 #[error("mp4: {0}")]
43 Mp4(std::sync::Arc<mp4_atom::Error>),
44
45 #[error("moq: {0}")]
46 Moq(#[from] moq_net::Error),
47
48 #[error("flac: {0}")]
49 Flac(#[from] crate::codec::flac::Error),
50
51 #[error("opus: {0}")]
52 Opus(#[from] crate::codec::opus::Error),
53
54 #[error("missing keyframe: a group must open on a keyframe")]
55 MissingKeyframe(#[from] crate::container::MissingKeyframe),
56
57 #[error("{0}")]
59 InvalidEnd(#[from] crate::container::InvalidEnd),
60
61 #[error("{0}")]
63 TimestampRewind(#[from] crate::container::TimestampRewind),
64
65 #[error("timestamp overflow")]
66 TimestampOverflow(#[from] moq_net::TimeOverflow),
67
68 #[error("no traf in moof")]
69 NoTraf,
70
71 #[error("no tfdt in traf")]
72 NoTfdt,
73
74 #[error("PTS overflow")]
75 PtsOverflow,
76
77 #[error("missing moof")]
78 NoMoof,
79
80 #[error("missing mdat")]
81 NoMdat,
82
83 #[error("missing moov")]
84 NoMoov,
85
86 #[error("no tracks in moov")]
87 NoTracks,
88
89 #[error("multiple tracks in moov, use Trak instead")]
90 MultipleTracks,
91
92 #[error("can't synthesize CMAF init for {0}")]
93 UnsupportedSynthesis(String),
94
95 #[error("subtitle tracks are not supported")]
96 UnsupportedSubtitle,
97
98 #[error("unknown track handler: {0:?}")]
99 UnknownTrackHandler([u8; 4]),
100
101 #[error("missing codec")]
102 MissingCodec,
103
104 #[error("multiple codecs")]
105 MultipleCodecs,
106
107 #[error("unknown codec: {0:?}")]
108 UnknownCodec(mp4_atom::FourCC),
109
110 #[error("unsupported codec: {0:?}")]
111 UnsupportedCodec(Box<mp4_atom::Codec>),
112
113 #[error("unsupported codec: MPEG2")]
114 UnsupportedMpeg2,
115
116 #[error("AAC sample entry missing DecoderSpecificInfo")]
118 MissingDecoderSpecific,
119
120 #[error("duplicate moof")]
121 DuplicateMoof,
122
123 #[error("duplicate moov")]
125 DuplicateMoov,
126
127 #[error("missing trun")]
128 MissingTrun,
129
130 #[error("missing tfdt")]
131 MissingTfdt,
132
133 #[error("video codec {0} needs a description (codec config record) to synthesize a CMAF init")]
134 MissingVideoDescription(String),
135
136 #[error("video track {0} missing in catalog")]
137 MissingVideoTrack(String),
138
139 #[error("missing video dimensions for codec: {0}")]
141 MissingVideoDimensions(String),
142
143 #[error("audio track {0} missing in catalog")]
144 MissingAudioTrack(String),
145
146 #[error("invalid data offset")]
147 InvalidDataOffset,
148
149 #[error("unknown track {0}")]
150 UnknownTrack(u32),
151
152 #[error("no keyframe at start of group")]
153 NoKeyframe,
154
155 #[error("track sample range {start}..{end} is out of bounds of mdat (len {len})")]
156 SampleRangeOutOfBounds { start: usize, end: usize, len: usize },
157
158 #[error("no catalog snapshot")]
159 NoCatalogSnapshot,
160
161 #[error("encode_fragment called with no frames")]
162 NoFrames,
163
164 #[error("audio codec {0} needs a description (AudioSpecificConfig) to synthesize a CMAF init")]
165 MissingAudioDescription(String),
166
167 #[error("multi-sample fragment has a non-final sample with no duration; DTS is unrecoverable")]
168 MissingSampleDuration,
169
170 #[error("timescale {0} does not fit the 32-bit mdhd field")]
173 TimescaleTooLarge(u64),
174
175 #[error("a CMAF rendition's timescale comes from its init segment and can't be overridden")]
178 TimescaleOverride,
179
180 #[error("sample duration {0} does not fit the 32-bit trun field")]
183 SampleDurationTooLarge(u64),
184
185 #[error("sample duration is shorter than one tick at timescale {0}")]
187 SampleDurationTooSmall(u64),
188
189 #[error("sample duration is not exactly representable at timescale {0}")]
191 SampleDurationInexact(u64),
192
193 #[error("duration-less video needs stated durations or presentation-ordered inference")]
195 MissingVideoDuration,
196
197 #[error(
201 "track {track}: fragment decode time {decode_time:?} does not advance past {previous:?}; \
202 ffmpeg repeats a tfdt when frag_every_frame interleaves audio and video, use -frag_duration instead"
203 )]
204 NonMonotonicDecodeTime {
205 track: u32,
207 decode_time: moq_net::Timestamp,
209 previous: moq_net::Timestamp,
211 },
212}
213
214impl From<mp4_atom::Error> for Error {
215 fn from(err: mp4_atom::Error) -> Self {
216 Error::Mp4(std::sync::Arc::new(err))
217 }
218}
219
220pub type Result<T> = std::result::Result<T, Error>;
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub(crate) enum Kind {
225 Audio,
226 Video,
227}
228
229impl Kind {
230 fn from_handler(handler: mp4_atom::FourCC) -> Result<Self> {
231 match handler.as_ref() {
232 b"soun" => Ok(Self::Audio),
233 b"vide" => Ok(Self::Video),
234 b"sbtl" => Err(Error::UnsupportedSubtitle),
235 _ => Err(Error::UnknownTrackHandler(handler.into())),
236 }
237 }
238
239 fn priority(self) -> u8 {
241 match self {
242 Self::Video => hang::catalog::PRIORITY.video,
243 Self::Audio => hang::catalog::PRIORITY.audio,
244 }
245 }
246}
247
248pub struct Wire {
257 trak: Box<mp4_atom::Trak>,
258}
259
260impl Wire {
261 pub fn new(trak: mp4_atom::Trak) -> Self {
263 Self { trak: Box::new(trak) }
264 }
265
266 pub fn from_init(init_data: &[u8]) -> Result<Self> {
268 use mp4_atom::DecodeMaybe;
269
270 let mut cursor = std::io::Cursor::new(init_data);
271 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
272 if let mp4_atom::Any::Moov(mut moov) = atom {
273 return match moov.trak.len() {
274 1 => Ok(Self::new(moov.trak.remove(0))),
275 0 => Err(Error::NoTracks),
276 _ => Err(Error::MultipleTracks),
277 };
278 }
279 }
280 Err(Error::NoMoov)
281 }
282
283 pub fn trak(&self) -> &mp4_atom::Trak {
284 &self.trak
285 }
286
287 fn kind(&self) -> Result<Kind> {
289 Kind::from_handler(self.trak.mdia.hdlr.handler)
290 }
291}
292
293impl Container for Wire {
294 fn kind(&self) -> crate::container::Kind {
295 match self.trak.mdia.hdlr.handler.as_ref() {
296 b"soun" => crate::container::Kind::Audio,
297 b"vide" => crate::container::Kind::Video,
298 _ => crate::container::Kind::Data,
299 }
300 }
301
302 type Error = Error;
303
304 fn write(&self, group: &mut moq_net::group::Producer, frames: &[Frame]) -> std::result::Result<(), Self::Error> {
305 let timescale = moq_net::Timescale::new(self.trak.mdia.mdhd.timescale as u64)?;
306 let track_id = self.trak.tkhd.track_id;
307 encode(
308 group,
309 frames,
310 FragmentInfo {
311 timescale,
312 track_id,
313 sequence_number: group.frame_count() as u32,
314 kind: self.kind()?,
315 },
316 )
317 }
318
319 fn poll_read(
320 &self,
321 group: &mut moq_net::group::Consumer,
322 waiter: &kio::Waiter,
323 ) -> Poll<std::result::Result<Option<Vec<Frame>>, Self::Error>> {
324 use std::task::ready;
325
326 let Some(frame) = ready!(group.poll_read_frame(waiter)?) else {
327 return Poll::Ready(Ok(None));
328 };
329
330 let timescale = moq_net::Timescale::new(self.trak.mdia.mdhd.timescale as u64)?;
331 Poll::Ready(Ok(Some(decode(frame.payload, timescale, self.kind()?)?)))
332 }
333}
334
335pub(crate) fn decode(data: Bytes, timescale: moq_net::Timescale, kind: Kind) -> Result<Vec<Frame>> {
341 use mp4_atom::DecodeMaybe;
342
343 let mut cursor = std::io::Cursor::new(&data);
344 let mut moof = None;
345 let mut mdat_data = None;
346
347 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
348 match atom {
349 mp4_atom::Any::Moof(m) => moof = Some(m),
350 mp4_atom::Any::Mdat(m) => mdat_data = Some(m.data),
351 _ => {}
352 }
353 }
354
355 let moof = moof.ok_or(Error::NoMoof)?;
356 let mdat_data = mdat_data.ok_or(Error::NoMdat)?;
357 let traf = moof.traf.first().ok_or(Error::NoTraf)?;
358 let tfdt = traf.tfdt.as_ref().ok_or(Error::NoTfdt)?;
359 let base_dts = tfdt.base_media_decode_time;
360
361 let default_size = traf.tfhd.default_sample_size;
362 let default_duration = traf.tfhd.default_sample_duration;
363
364 let total_samples: usize = traf.trun.iter().map(|t| t.entries.len()).sum();
368
369 let mut frames = Vec::new();
370 let mut offset = 0usize;
371 let mut dts = base_dts;
372 let mut sample_index = 0usize;
373
374 for trun in &traf.trun {
375 for entry in &trun.entries {
376 let size = entry.size.or(default_size).unwrap_or(0) as usize;
377 let end = offset + size;
378
379 if end > mdat_data.len() {
380 return Err(Error::SampleRangeOutOfBounds {
381 start: offset,
382 end,
383 len: mdat_data.len(),
384 });
385 }
386
387 let cts = entry.cts.unwrap_or_default() as i64;
388 let pts = dts.checked_add_signed(cts).ok_or(Error::PtsOverflow)?;
389 let timestamp = Timestamp::new(pts, timescale)?;
391 let payload = Bytes::copy_from_slice(&mdat_data[offset..end]);
392 let flags = entry.flags.unwrap_or(0);
393 let keyframe = kind == Kind::Video && (flags >> 24) & 0x3 == 0x2;
395
396 let sample_duration = entry.duration.or(default_duration).filter(|d| *d != 0);
399
400 let is_last = sample_index + 1 == total_samples;
403 if sample_duration.is_none() && !is_last {
404 return Err(Error::MissingSampleDuration);
405 }
406
407 let duration = sample_duration
408 .map(|d| Timestamp::new(d as u64, timescale))
409 .transpose()?;
410
411 frames.push(Frame {
412 timestamp,
413 payload,
414 keyframe,
415 duration,
416 });
417
418 offset = end;
419 dts += sample_duration.unwrap_or(0) as u64;
420 sample_index += 1;
421 }
422 }
423
424 Ok(frames)
425}
426
427fn encode(group: &mut moq_net::group::Producer, frames: &[Frame], info: FragmentInfo) -> Result<()> {
428 if frames.is_empty() {
429 return Ok(());
430 }
431
432 let bytes = encode_fragment(info, frames)?;
433 let mut writer = group.create_frame(moq_net::frame::Info {
436 size: bytes.len() as u64,
437 timestamp: frames[0].timestamp,
438 })?;
439 writer.write(bytes)?;
440 writer.finish()?;
441
442 Ok(())
443}
444
445#[derive(Debug, Clone, Copy)]
450pub(crate) struct FragmentInfo {
451 pub track_id: u32,
453 pub timescale: moq_net::Timescale,
455 pub sequence_number: u32,
457 pub kind: Kind,
460}
461
462pub(crate) fn encode_fragment(info: FragmentInfo, frames: &[Frame]) -> Result<Bytes> {
470 let Some(first) = frames.first() else {
471 return Ok(Bytes::new());
472 };
473 encode_at(info, base_ticks(first, info.timescale)?, frames)
474}
475
476fn base_ticks(frame: &Frame, timescale: moq_net::Timescale) -> Result<u64> {
482 timestamp_ticks(frame.timestamp, timescale)
483}
484
485fn timestamp_ticks(timestamp: Timestamp, timescale: moq_net::Timescale) -> Result<u64> {
487 let source_scale = u128::from(timestamp.scale().as_u64());
488 let scaled = u128::from(timestamp.value()) * u128::from(timescale.as_u64());
489 let ticks = (scaled + source_scale / 2) / source_scale;
490 u64::try_from(ticks).map_err(|_| Error::PtsOverflow)
491}
492
493fn encode_at(info: FragmentInfo, base_dts: u64, frames: &[Frame]) -> Result<Bytes> {
510 let FragmentInfo {
511 track_id,
512 timescale,
513 sequence_number,
514 kind,
515 } = info;
516
517 use mp4_atom::Encode;
518
519 if frames.is_empty() {
520 return Ok(Bytes::new());
521 }
522
523 let mut dts = base_dts;
524
525 let entries: Vec<_> = frames
526 .iter()
527 .map(|f| {
528 let flags = if kind == Kind::Audio || f.keyframe {
529 0x0200_0000
530 } else {
531 0x0001_0000
532 };
533 let duration = f.duration.map(|d| trun_duration(d, timescale)).transpose()?;
536 let pts = i128::from(timestamp_ticks(f.timestamp, timescale)?);
537 let cts = pts - i128::from(dts);
538 let cts = i32::try_from(cts).map_err(|_| Error::PtsOverflow)?;
539
540 if let Some(duration) = duration {
543 dts = dts.checked_add(u64::from(duration)).ok_or(Error::PtsOverflow)?;
544 }
545
546 Ok(mp4_atom::TrunEntry {
547 duration,
548 size: Some(f.payload.len() as u32),
549 flags: Some(flags),
550 cts: (cts != 0).then_some(cts),
551 })
552 })
553 .collect::<Result<_>>()?;
554
555 let mdat_data: Vec<u8> = frames.iter().flat_map(|f| f.payload.iter().copied()).collect();
556
557 let build_moof = |data_offset| mp4_atom::Moof {
558 mfhd: mp4_atom::Mfhd { sequence_number },
559 traf: vec![mp4_atom::Traf {
560 tfhd: mp4_atom::Tfhd {
561 track_id,
562 ..Default::default()
563 },
564 tfdt: Some(mp4_atom::Tfdt {
565 base_media_decode_time: base_dts,
566 }),
567 trun: vec![mp4_atom::Trun {
568 data_offset: Some(data_offset),
569 entries: entries.clone(),
570 }],
571 ..Default::default()
572 }],
573 };
574
575 let mut buf = Vec::new();
577 build_moof(0).encode(&mut buf)?;
578 let moof_size = buf.len();
579
580 buf.clear();
582 build_moof((moof_size + 8) as i32).encode(&mut buf)?;
583
584 let mdat = mp4_atom::Mdat { data: mdat_data };
585 mdat.encode(&mut buf)?;
586
587 Ok(Bytes::from(buf))
588}
589
590fn trun_duration(duration: Timestamp, timescale: moq_net::Timescale) -> Result<u32> {
592 let source_timescale = duration.scale().as_u64();
593 let output_timescale = timescale.as_u64();
594 let scaled = u128::from(duration.value()) * u128::from(output_timescale);
595 let ticks = scaled / u128::from(source_timescale);
596 if !duration.is_zero() && ticks == 0 {
597 return Err(Error::SampleDurationTooSmall(timescale.as_u64()));
598 }
599 if scaled % u128::from(source_timescale) != 0 {
600 return Err(Error::SampleDurationInexact(output_timescale));
601 }
602 u32::try_from(ticks).map_err(|_| Error::SampleDurationTooLarge(u64::try_from(ticks).unwrap_or(u64::MAX)))
603}
604
605pub(crate) fn synthesize_video_trak(
613 track_id: u32,
614 timescale: u64,
615 config: &VideoConfig,
616 description: Option<&[u8]>,
617) -> Result<mp4_atom::Trak> {
618 if !matches!(
619 config.codec,
620 VideoCodec::H264(_) | VideoCodec::H265(_) | VideoCodec::AV1(_) | VideoCodec::VP8 | VideoCodec::VP9(_)
621 ) {
622 return Err(Error::UnsupportedSynthesis(format!("video codec {:?}", config.codec)));
623 }
624
625 let width = u16::try_from(
626 config
627 .coded_width
628 .ok_or_else(|| Error::MissingVideoDimensions(config.codec.to_string()))?,
629 )
630 .map_err(|_| Error::MissingVideoDimensions(config.codec.to_string()))?;
631 let height = u16::try_from(
632 config
633 .coded_height
634 .ok_or_else(|| Error::MissingVideoDimensions(config.codec.to_string()))?,
635 )
636 .map_err(|_| Error::MissingVideoDimensions(config.codec.to_string()))?;
637 if width == 0 || height == 0 {
638 return Err(Error::MissingVideoDimensions(config.codec.to_string()));
639 }
640 let visual = mp4_atom::Visual {
641 data_reference_index: 1,
642 width,
643 height,
644 ..Default::default()
645 };
646
647 let require_description = || description.ok_or_else(|| Error::MissingVideoDescription(config.codec.to_string()));
649
650 let sample_entry = match &config.codec {
651 VideoCodec::H264(_) => {
652 let mut cursor = std::io::Cursor::new(require_description()?);
653 let avcc = mp4_atom::Avcc::decode_body(&mut cursor).map_err(Error::from)?;
654 mp4_atom::Codec::from(mp4_atom::Avc1 {
655 visual,
656 avcc,
657 ..Default::default()
658 })
659 }
660 VideoCodec::H265(h265) => {
661 let mut cursor = std::io::Cursor::new(require_description()?);
662 let hvcc = mp4_atom::Hvcc::decode_body(&mut cursor).map_err(Error::from)?;
663 if h265.in_band {
665 mp4_atom::Codec::from(mp4_atom::Hev1 {
666 visual,
667 hvcc,
668 ..Default::default()
669 })
670 } else {
671 mp4_atom::Codec::from(mp4_atom::Hvc1 {
672 visual,
673 hvcc,
674 ..Default::default()
675 })
676 }
677 }
678 VideoCodec::AV1(av1) => mp4_atom::Codec::from(mp4_atom::Av01 {
679 visual,
680 av1c: crate::codec::av1::av1c_from_av1(av1),
681 ..Default::default()
682 }),
683 VideoCodec::VP8 => mp4_atom::Codec::from(mp4_atom::Vp08 {
684 visual,
685 vpcc: crate::codec::vp8::vpcc(),
686 ..Default::default()
687 }),
688 VideoCodec::VP9(vp9) => mp4_atom::Codec::from(mp4_atom::Vp09 {
689 visual,
690 vpcc: crate::codec::vp9::vpcc(vp9),
691 ..Default::default()
692 }),
693 other => unreachable!("unsupported codecs rejected before geometry synthesis: {other:?}"),
694 };
695
696 Ok(build_video_trak(
697 track_id,
698 mdhd_timescale(timescale)?,
699 sample_entry,
700 width,
701 height,
702 ))
703}
704
705pub(crate) fn synthesize_audio_trak(track_id: u32, timescale: u64, config: &AudioConfig) -> Result<mp4_atom::Trak> {
707 use mp4_atom::Decode;
708
709 let audio = mp4_atom::Audio {
710 data_reference_index: 1,
711 channel_count: config.channel_count as u16,
712 sample_size: 16,
713 sample_rate: mp4_atom::FixedPoint::from(config.sample_rate as u16),
714 };
715
716 let sample_entry = match &config.codec {
717 AudioCodec::Opus => {
718 let head = match &config.description {
719 Some(description) => {
720 let head = crate::codec::opus::Config::parse(&mut description.as_ref())?;
721 if head.mapping_family != 0 {
723 return Err(crate::codec::opus::Error::UnsupportedMappingFamily(head.mapping_family).into());
724 }
725 head
726 }
727 None => crate::codec::opus::Config::new(config.sample_rate, config.channel_count),
728 };
729 mp4_atom::Codec::from(mp4_atom::Opus {
730 audio,
731 dops: mp4_atom::Dops {
732 output_channel_count: config.channel_count as u8,
733 pre_skip: head.pre_skip,
734 input_sample_rate: head.sample_rate,
735 output_gain: head.output_gain,
736 },
737 btrt: None,
738 })
739 }
740 AudioCodec::AAC(_) => {
741 let description = config
746 .description
747 .as_ref()
748 .ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
749 let mut cursor = std::io::Cursor::new(description.as_ref());
750 let dec_specific = mp4_atom::esds::DecoderSpecific::decode(&mut cursor)?;
751
752 let bitrate = config.bitrate.and_then(|b| u32::try_from(b).ok()).filter(|b| *b > 0);
760 let (max_bitrate, avg_bitrate) = match bitrate {
761 Some(bitrate) => (bitrate, bitrate),
762 None => (256_000, 128_000),
763 };
764 mp4_atom::Codec::from(mp4_atom::Mp4a {
765 audio,
766 esds: mp4_atom::Esds {
767 es_desc: mp4_atom::esds::EsDescriptor {
768 es_id: 0,
770 dec_config: mp4_atom::esds::DecoderConfig {
771 object_type_indication: 0x40, stream_type: 0x05, up_stream: 0,
774 buffer_size_db: mp4_atom::u24::from([0x00, 0x60, 0x00]),
776 max_bitrate,
777 avg_bitrate,
778 dec_specific: Some(dec_specific),
779 },
780 sl_config: Default::default(),
781 },
782 },
783 btrt: None,
784 taic: None,
785 })
786 }
787 AudioCodec::Flac => {
788 let description = config
791 .description
792 .as_ref()
793 .ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
794 let info = crate::codec::flac::Config::parse(&mut description.as_ref())?;
795
796 let stream_info = mp4_atom::FlacMetadataBlock::StreamInfo {
797 minimum_block_size: info.min_block_size,
798 maximum_block_size: info.max_block_size,
799 minimum_frame_size: info.min_frame_size.min(0xFF_FFFF).try_into().expect("fits in u24"),
801 maximum_frame_size: info.max_frame_size.min(0xFF_FFFF).try_into().expect("fits in u24"),
802 sample_rate: info.sample_rate,
803 num_channels_minus_one: info.channel_count.saturating_sub(1) as u8,
804 bits_per_sample_minus_one: info.bits_per_sample.saturating_sub(1) as u8,
805 number_of_interchannel_samples: info.total_samples,
806 md5_checksum: info.md5.to_vec(),
807 };
808
809 mp4_atom::Codec::from(mp4_atom::Flac {
810 audio,
811 dfla: mp4_atom::Dfla {
812 blocks: vec![stream_info],
813 },
814 })
815 }
816 other => return Err(Error::UnsupportedSynthesis(format!("audio codec {:?}", other))),
817 };
818
819 Ok(build_audio_trak(track_id, mdhd_timescale(timescale)?, sample_entry))
820}
821
822const UNKNOWN_DURATION: u64 = u64::MAX;
826
827fn build_video_trak(
828 track_id: u32,
829 timescale: u32,
830 sample_entry: mp4_atom::Codec,
831 width: u16,
832 height: u16,
833) -> mp4_atom::Trak {
834 mp4_atom::Trak {
835 tkhd: mp4_atom::Tkhd {
836 track_id,
837 enabled: true,
838 in_movie: true,
841 duration: UNKNOWN_DURATION,
842 width: mp4_atom::FixedPoint::from(width),
843 height: mp4_atom::FixedPoint::from(height),
844 ..Default::default()
845 },
846 mdia: build_mdia(timescale, b"vide", true, sample_entry),
847 ..Default::default()
848 }
849}
850
851fn build_audio_trak(track_id: u32, timescale: u32, sample_entry: mp4_atom::Codec) -> mp4_atom::Trak {
852 mp4_atom::Trak {
853 tkhd: mp4_atom::Tkhd {
854 track_id,
855 enabled: true,
856 in_movie: true,
857 duration: UNKNOWN_DURATION,
858 volume: mp4_atom::FixedPoint::from(1),
861 ..Default::default()
862 },
863 mdia: build_mdia(timescale, b"soun", false, sample_entry),
864 ..Default::default()
865 }
866}
867
868pub(crate) fn encode_init(
875 ftyp: Option<mp4_atom::Ftyp>,
876 mut traks: Vec<mp4_atom::Trak>,
877 trexs: Vec<mp4_atom::Trex>,
878) -> Result<Bytes> {
879 use mp4_atom::Encode;
880
881 let ftyp = ftyp.unwrap_or(mp4_atom::Ftyp {
882 major_brand: b"isom".into(),
883 minor_version: 0x200,
884 compatible_brands: vec![b"isom".into(), b"iso6".into(), b"mp41".into()],
885 });
886 for trak in &mut traks {
890 if let [mp4_atom::Codec::Opus(opus)] = trak.mdia.minf.stbl.stsd.codecs.as_slice() {
891 let skip = u64::from(opus.dops.pre_skip) * u64::from(trak.mdia.mdhd.timescale) / 48_000;
892 trak.edts = (skip != 0).then(|| mp4_atom::Edts {
893 elst: Some(mp4_atom::Elst {
894 entries: vec![mp4_atom::ElstEntry {
895 segment_duration: 0,
897 media_time: Some(skip),
898 media_rate: mp4_atom::FixedPoint::from(1),
899 }],
900 }),
901 });
902 }
903 }
904 let timescale = traks.first().map(|t| t.mdia.mdhd.timescale).unwrap_or(1000);
905 let next_track_id = traks.iter().map(|t| t.tkhd.track_id).max().unwrap_or(0) + 1;
906
907 let moov = mp4_atom::Moov {
908 mvhd: mp4_atom::Mvhd {
909 timescale,
910 duration: UNKNOWN_DURATION,
911 rate: mp4_atom::FixedPoint::from(1),
914 volume: mp4_atom::FixedPoint::from(1),
915 next_track_id,
917 ..Default::default()
918 },
919 trak: traks,
920 mvex: (!trexs.is_empty()).then(|| mp4_atom::Mvex {
921 trex: trexs,
922 ..Default::default()
923 }),
924 ..Default::default()
925 };
926
927 let mut buf = Vec::new();
928 ftyp.encode(&mut buf)?;
929 moov.encode(&mut buf)?;
930 Ok(Bytes::from(buf))
931}
932
933fn mdhd_timescale(timescale: u64) -> Result<u32> {
939 u32::try_from(timescale).map_err(|_| Error::TimescaleTooLarge(timescale))
940}
941
942fn build_mdia(timescale: u32, handler: &[u8; 4], is_video: bool, sample_entry: mp4_atom::Codec) -> mp4_atom::Mdia {
943 mp4_atom::Mdia {
944 mdhd: mp4_atom::Mdhd {
945 timescale,
946 ..Default::default()
947 },
948 hdlr: mp4_atom::Hdlr {
949 handler: mp4_atom::FourCC::new(handler),
950 name: String::new(),
951 },
952 minf: mp4_atom::Minf {
953 vmhd: is_video.then(mp4_atom::Vmhd::default),
954 smhd: (!is_video).then(mp4_atom::Smhd::default),
955 dinf: mp4_atom::Dinf {
956 dref: mp4_atom::Dref {
957 urls: vec![mp4_atom::Url::default()],
958 },
959 },
960 stbl: mp4_atom::Stbl {
961 stsd: mp4_atom::Stsd {
962 codecs: vec![sample_entry],
963 },
964 ..Default::default()
965 },
966 ..Default::default()
967 },
968 }
969}
970
971pub(crate) fn default_video_timescale(config: &VideoConfig) -> u64 {
982 usable_video_framerate(config)
983 .and_then(select_video_timescale)
984 .unwrap_or(90_000)
985}
986
987pub(crate) fn usable_video_framerate(config: &VideoConfig) -> Option<f64> {
989 config
990 .framerate
991 .filter(|fps| fps.is_finite() && *fps > 0.0 && (*fps * 1000.0) as u64 > 0)
992 .filter(|fps| select_video_timescale(*fps).is_some())
993}
994
995fn select_video_timescale(framerate: f64) -> Option<u64> {
997 let preferred = (framerate * 1000.0) as u64;
998
999 let frame = Duration::from_secs_f64(1.0 / framerate);
1000 for timescale in [preferred, (framerate * 1001.0).round() as u64] {
1001 if duration_fits_trun(frame, timescale) {
1002 return Some(timescale);
1003 }
1004 }
1005
1006 const NANOS_PER_SECOND: u128 = 1_000_000_000;
1007 let exact = NANOS_PER_SECOND / gcd(frame.as_nanos(), NANOS_PER_SECOND);
1008 let exact = u64::try_from(exact).ok()?;
1009 if duration_fits_trun(frame, exact) {
1010 return Some(exact);
1011 }
1012 if let Some(timescale) = rational_timescale(frame) {
1013 return Some(timescale);
1014 }
1015
1016 let max_scale = u128::from(u32::MAX)
1017 .checked_mul(NANOS_PER_SECOND)?
1018 .checked_div(frame.as_nanos())?
1019 .min(u128::from(u32::MAX));
1020 let max_scale = u64::try_from(max_scale).ok()?;
1021 duration_fits_trun(frame, max_scale).then_some(max_scale)
1022}
1023
1024fn rational_timescale(duration: Duration) -> Option<u64> {
1026 const NANOS_PER_SECOND: u128 = 1_000_000_000;
1027 let mut numerator = duration.as_nanos();
1028 let mut denominator = NANOS_PER_SECOND;
1029 let (mut previous_ticks, mut ticks) = (0_u128, 1_u128);
1030 let (mut previous_scale, mut scale) = (1_u128, 0_u128);
1031
1032 while denominator != 0 {
1033 let coefficient = numerator / denominator;
1034 let next_ticks = coefficient.checked_mul(ticks)?.checked_add(previous_ticks)?;
1035 let next_scale = coefficient.checked_mul(scale)?.checked_add(previous_scale)?;
1036 if next_ticks > u128::from(u32::MAX) || next_scale > u128::from(u32::MAX) {
1037 break;
1038 }
1039
1040 let candidate = u64::try_from(next_scale).ok()?;
1041 if duration_fits_trun(duration, candidate) {
1042 return Some(candidate);
1043 }
1044
1045 (previous_ticks, ticks) = (ticks, next_ticks);
1046 (previous_scale, scale) = (scale, next_scale);
1047 (numerator, denominator) = (denominator, numerator % denominator);
1048 }
1049
1050 None
1051}
1052
1053fn duration_fits_trun(duration: Duration, timescale: u64) -> bool {
1055 timescale > 0 && rounded_duration_ticks(duration, timescale).is_some_and(|ticks| u32::try_from(ticks).is_ok())
1056}
1057
1058fn rounded_duration_ticks(duration: Duration, timescale: u64) -> Option<u64> {
1060 const NANOS_PER_SECOND: u128 = 1_000_000_000;
1061 let scaled = duration.as_nanos().checked_mul(u128::from(timescale))?;
1062 let rounded = scaled.checked_add(NANOS_PER_SECOND / 2)? / NANOS_PER_SECOND;
1063 let exact = rounded.checked_mul(NANOS_PER_SECOND)?;
1064 if scaled.abs_diff(exact) > u128::from(timescale) {
1065 return None;
1066 }
1067 u64::try_from(rounded).ok()
1068}
1069
1070fn gcd(mut a: u128, mut b: u128) -> u128 {
1072 while b != 0 {
1073 (a, b) = (b, a % b);
1074 }
1075 a
1076}
1077
1078#[cfg(test)]
1081pub(crate) fn timeline(fragment: &Bytes) -> (u64, Vec<i32>) {
1082 let traf = first_traf(fragment);
1083 let cts = traf.trun[0].entries.iter().map(|e| e.cts.unwrap_or_default()).collect();
1084 (traf.tfdt.as_ref().unwrap().base_media_decode_time, cts)
1085}
1086
1087#[cfg(test)]
1089pub(crate) fn sample_durations(fragment: &Bytes) -> Vec<Option<u32>> {
1090 first_traf(fragment).trun[0]
1091 .entries
1092 .iter()
1093 .map(|e| e.duration)
1094 .collect()
1095}
1096
1097#[cfg(test)]
1098fn first_traf(fragment: &Bytes) -> mp4_atom::Traf {
1099 use mp4_atom::DecodeMaybe;
1100
1101 let mut cursor = std::io::Cursor::new(fragment.as_ref());
1102 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
1103 if let mp4_atom::Any::Moof(moof) = atom {
1104 return moof.traf.into_iter().next().expect("a traf");
1105 }
1106 }
1107 panic!("no moof");
1108}
1109
1110#[cfg(test)]
1111mod tests {
1112 use super::*;
1113
1114 fn ts(micros: u64) -> Timestamp {
1115 Timestamp::from_micros(micros).unwrap()
1116 }
1117
1118 fn info(track_id: u32, timescale: moq_net::Timescale, sequence_number: u32) -> FragmentInfo {
1119 FragmentInfo {
1120 track_id,
1121 timescale,
1122 sequence_number,
1123 kind: Kind::Video,
1124 }
1125 }
1126
1127 fn aac_config(bitrate: Option<u64>) -> AudioConfig {
1129 let mut config = AudioConfig::new(AudioCodec::AAC(hang::catalog::AAC { profile: 2 }), 44_100, 2);
1130 config.description = Some(Bytes::from_static(&[0x12, 0x10]));
1131 config.bitrate = bitrate;
1132 config
1133 }
1134
1135 fn moov(init: &Bytes) -> mp4_atom::Moov {
1138 use mp4_atom::DecodeMaybe;
1139
1140 let mut cursor = std::io::Cursor::new(init.as_ref());
1141 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
1142 if let mp4_atom::Any::Moov(moov) = atom {
1143 return moov;
1144 }
1145 }
1146 panic!("no moov");
1147 }
1148
1149 fn dec_config(trak: &mp4_atom::Trak) -> mp4_atom::esds::DecoderConfig {
1150 match &trak.mdia.minf.stbl.stsd.codecs[0] {
1151 mp4_atom::Codec::Mp4a(mp4a) => mp4a.esds.es_desc.dec_config.clone(),
1152 other => panic!("expected mp4a, got {other:?}"),
1153 }
1154 }
1155
1156 #[test]
1160 fn synthesized_aac_init_has_non_zero_bitrates() {
1161 let inferred = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap());
1162 assert_ne!(u32::from(inferred.buffer_size_db), 0);
1163 assert_ne!(inferred.max_bitrate, 0);
1164 assert_ne!(inferred.avg_bitrate, 0);
1165
1166 let stated = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(Some(96_000))).unwrap());
1167 assert_eq!(stated.max_bitrate, 96_000, "the catalog's bitrate wins");
1168 assert_eq!(stated.avg_bitrate, 96_000);
1169
1170 for unusable in [Some(0), Some(u64::from(u32::MAX) + 1)] {
1173 let config = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(unusable)).unwrap());
1174 assert_eq!(config.max_bitrate, inferred.max_bitrate, "{unusable:?}");
1175 assert_eq!(config.avg_bitrate, inferred.avg_bitrate, "{unusable:?}");
1176 }
1177 }
1178
1179 #[test]
1182 fn default_video_timescale_ignores_an_unusable_framerate() {
1183 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1184 for unusable in [0.0, -30.0, f64::NAN, f64::INFINITY, 0.0005] {
1185 config.framerate = Some(unusable);
1186 assert_eq!(default_video_timescale(&config), 90_000, "{unusable}");
1187 }
1188
1189 config.framerate = Some(30.0);
1190 assert_eq!(default_video_timescale(&config), 30_000);
1191
1192 config.framerate = Some(30_000.0 / 1001.0);
1193 assert_eq!(default_video_timescale(&config), 30_000);
1194 }
1195
1196 #[test]
1197 fn default_video_timescale_keeps_low_cadence_within_trun() {
1198 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1199 for framerate in [0.2001, 0.0011] {
1200 config.framerate = Some(framerate);
1201 let timescale = default_video_timescale(&config);
1202 let duration = Duration::from_secs_f64(1.0 / framerate);
1203 let ticks = rounded_duration_ticks(duration, timescale).unwrap();
1204
1205 assert!(timescale <= u64::from(u32::MAX));
1206 assert!(ticks <= u64::from(u32::MAX));
1207 }
1208
1209 assert_eq!(default_video_timescale(&config), 11);
1210 }
1211
1212 #[test]
1215 fn synthesized_init_declares_unknown_duration() {
1216 let trak = synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap();
1217 assert_eq!(trak.tkhd.duration, u64::MAX);
1218
1219 let init = encode_init(None, vec![trak], Vec::new()).unwrap();
1220 let moov = moov(&init);
1221 assert_eq!(moov.mvhd.duration, u64::MAX);
1222 assert_eq!(moov.trak[0].tkhd.duration, u64::MAX);
1223 }
1224
1225 #[test]
1229 fn opus_init_maps_hang_timestamps_to_media() {
1230 for timescale in [48_000, 1_000_000] {
1231 for pre_skip in [0, 312, u16::MAX] {
1232 let mut config = AudioConfig::new(AudioCodec::Opus, 48_000, 2);
1233 config.description = Some(
1234 crate::codec::opus::Config::new(48_000, 2)
1235 .with_pre_skip(pre_skip)
1236 .encode()
1237 .unwrap(),
1238 );
1239 for source_edit in [false, true] {
1240 let mut trak = synthesize_audio_trak(1, timescale, &config).unwrap();
1241 if source_edit {
1242 trak.edts = Some(mp4_atom::Edts {
1243 elst: Some(mp4_atom::Elst {
1244 entries: vec![mp4_atom::ElstEntry {
1245 media_time: Some(96000),
1246 ..Default::default()
1247 }],
1248 }),
1249 });
1250 let mut init = Vec::new();
1251 mp4_atom::Encode::encode(
1252 &mp4_atom::Moov {
1253 trak: vec![trak],
1254 ..Default::default()
1255 },
1256 &mut init,
1257 )
1258 .unwrap();
1259 let mut traks = Vec::new();
1260 extract_init(&Bytes::from(init), 1, &mut None, &mut traks, &mut Vec::new()).unwrap();
1261 trak = traks.pop().unwrap();
1262 }
1263 let init = encode_init(None, vec![trak], Vec::new()).unwrap();
1264 let moov = moov(&init);
1265 let trak = &moov.trak[0];
1266 let skip = u64::from(pre_skip) * timescale / 48_000;
1267 if skip == 0 {
1268 assert!(trak.edts.is_none());
1269 } else {
1270 let entries = &trak.edts.as_ref().unwrap().elst.as_ref().unwrap().entries;
1271 assert_eq!(entries.len(), 1);
1272 assert_eq!(entries[0].media_time, Some(skip));
1273 assert_eq!(entries[0].segment_duration, 0, "the live duration is implicit");
1274 assert_eq!(entries[0].media_rate, mp4_atom::FixedPoint::from(1));
1275 }
1276 }
1277 }
1278 }
1279 }
1280
1281 #[test]
1283 fn non_opus_init_has_no_edit() {
1284 let trak = synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap();
1285 let init = encode_init(None, vec![trak], Vec::new()).unwrap();
1286 assert!(moov(&init).trak[0].edts.is_none());
1287 }
1288
1289 #[test]
1293 fn synthesized_init_headers_describe_a_playable_presentation() {
1294 let audio = synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap();
1295 let init = encode_init(None, vec![audio], Vec::new()).unwrap();
1296 let moov = moov(&init);
1297
1298 assert_eq!(moov.mvhd.rate.integer(), 1, "normal playback rate");
1299 assert_eq!(moov.mvhd.volume.integer(), 1, "full volume");
1300 assert_eq!(moov.mvhd.next_track_id, 2, "past the only track id");
1301
1302 let tkhd = &moov.trak[0].tkhd;
1303 assert!(tkhd.enabled && tkhd.in_movie, "flags 0x000003");
1304 assert_eq!(tkhd.volume.integer(), 1, "an audio track carries the volume");
1305 }
1306
1307 #[test]
1309 fn synthesized_video_init_sets_the_track_flags() {
1310 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1311 config.coded_width = Some(320);
1312 config.coded_height = Some(240);
1313 config.framerate = Some(30.0);
1314 let video = synthesize_video_trak(1, 30_000, &config, None).unwrap();
1315 let init = encode_init(None, vec![video], Vec::new()).unwrap();
1316 let moov = moov(&init);
1317
1318 let tkhd = &moov.trak[0].tkhd;
1319 assert!(tkhd.enabled && tkhd.in_movie, "flags 0x000003");
1320 assert_eq!(tkhd.volume.integer(), 0);
1321 }
1322
1323 #[test]
1324 fn synthesized_video_init_rejects_missing_dimensions() {
1325 let config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1326 let error = synthesize_video_trak(1, 30_000, &config, None).unwrap_err();
1327 assert!(matches!(error, Error::MissingVideoDimensions(_)));
1328 }
1329
1330 #[test]
1331 fn decode_reads_trun_sample_duration() {
1332 use mp4_atom::Encode;
1333
1334 let timescale = moq_net::Timescale::MICRO;
1338 let moof = mp4_atom::Moof {
1339 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
1340 traf: vec![mp4_atom::Traf {
1341 tfhd: mp4_atom::Tfhd {
1342 track_id: 1,
1343 ..Default::default()
1344 },
1345 tfdt: Some(mp4_atom::Tfdt {
1346 base_media_decode_time: 0,
1347 }),
1348 trun: vec![mp4_atom::Trun {
1349 data_offset: Some(0),
1350 entries: vec![
1351 mp4_atom::TrunEntry {
1352 size: Some(2),
1353 duration: Some(33_333),
1354 ..Default::default()
1355 },
1356 mp4_atom::TrunEntry {
1357 size: Some(2),
1358 duration: Some(33_333),
1359 ..Default::default()
1360 },
1361 ],
1362 }],
1363 ..Default::default()
1364 }],
1365 };
1366
1367 let mut buf = Vec::new();
1368 moof.encode(&mut buf).unwrap();
1369 mp4_atom::Mdat {
1370 data: vec![0xDE, 0xAD, 0xBE, 0xEF],
1371 }
1372 .encode(&mut buf)
1373 .unwrap();
1374
1375 let frames = decode(Bytes::from(buf), timescale, Kind::Video).unwrap();
1376 assert_eq!(frames.len(), 2);
1377 assert_eq!(frames[0].timestamp, ts(0));
1378 assert_eq!(frames[0].duration, Some(ts(33_333)));
1379 assert_eq!(frames[1].timestamp, ts(33_333));
1380 assert_eq!(frames[1].duration, Some(ts(33_333)));
1381 }
1382
1383 #[test]
1384 fn duration_round_trips_through_encode() {
1385 let timescale = moq_net::Timescale::MICRO;
1387 let input = vec![Frame {
1388 timestamp: ts(0),
1389 payload: Bytes::from_static(&[0xDE, 0xAD]),
1390 keyframe: true,
1391 duration: Some(ts(33_333)),
1392 }];
1393
1394 let fragment = encode_fragment(info(1, timescale, 0), &input).unwrap();
1395 let frames = decode(fragment, timescale, Kind::Video).unwrap();
1396
1397 assert_eq!(frames.len(), 1);
1398 assert_eq!(frames[0].duration, Some(ts(33_333)));
1399 }
1400
1401 #[test]
1404 fn encode_fragment_rejects_a_duration_too_large_for_trun() {
1405 let timescale = moq_net::Timescale::new(u64::from(u32::MAX)).unwrap();
1406 let over = u64::from(u32::MAX) + 1;
1407 let frame = Frame {
1408 timestamp: Timestamp::from_scale(0, timescale.as_u64()).unwrap(),
1409 payload: Bytes::from_static(&[0xDE, 0xAD]),
1410 keyframe: true,
1411 duration: Some(Timestamp::from_scale(over, timescale.as_u64()).unwrap()),
1412 };
1413
1414 let err = encode_fragment(info(1, timescale, 0), std::slice::from_ref(&frame)).unwrap_err();
1415 assert!(matches!(err, Error::SampleDurationTooLarge(ticks) if ticks == over));
1416
1417 let largest = Frame {
1418 duration: Some(Timestamp::from_scale(u64::from(u32::MAX), timescale.as_u64()).unwrap()),
1419 ..frame
1420 };
1421 let fragment = encode_fragment(info(1, timescale, 0), &[largest]).unwrap();
1422 assert_eq!(sample_durations(&fragment), vec![Some(u32::MAX)]);
1423 }
1424
1425 #[test]
1428 fn encode_fragment_rejects_a_duration_shorter_than_one_tick() {
1429 let timescale = moq_net::Timescale::SECOND;
1430 let frame = Frame {
1431 timestamp: Timestamp::from_secs(0).unwrap(),
1432 payload: Bytes::from_static(&[0xDE, 0xAD]),
1433 keyframe: true,
1434 duration: Some(Timestamp::from_millis(33).unwrap()),
1435 };
1436
1437 let err = encode_fragment(info(1, timescale, 0), std::slice::from_ref(&frame)).unwrap_err();
1438 assert!(matches!(err, Error::SampleDurationTooSmall(1)));
1439
1440 let one_tick = Frame {
1441 duration: Some(Timestamp::from_secs(1).unwrap()),
1442 ..frame
1443 };
1444 let fragment = encode_fragment(info(1, timescale, 0), &[one_tick]).unwrap();
1445 assert_eq!(sample_durations(&fragment), vec![Some(1)]);
1446 }
1447
1448 #[test]
1451 fn encode_fragment_rejects_an_inexact_sample_duration() {
1452 let input_scale = moq_net::Timescale::new(24).unwrap();
1453 let output_scale = moq_net::Timescale::MILLI;
1454 let frame = Frame {
1455 timestamp: Timestamp::new(0, input_scale).unwrap(),
1456 payload: Bytes::from_static(&[0xDE, 0xAD]),
1457 keyframe: true,
1458 duration: Some(Timestamp::new(1, input_scale).unwrap()),
1459 };
1460
1461 let err = encode_fragment(info(1, output_scale, 0), std::slice::from_ref(&frame)).unwrap_err();
1462 assert!(matches!(err, Error::SampleDurationInexact(1_000)));
1463
1464 let exact_scale = moq_net::Timescale::new(24_000).unwrap();
1465 let fragment = encode_fragment(info(1, exact_scale, 0), &[frame]).unwrap();
1466 assert_eq!(sample_durations(&fragment), vec![Some(1_000)]);
1467 }
1468
1469 #[test]
1472 fn encode_fragment_rejects_a_pts_too_large_for_tfdt() {
1473 let timescale = moq_net::Timescale::new(u64::from(u32::MAX)).unwrap();
1474 let frame = Frame {
1475 timestamp: Timestamp::from_secs(1 << 40).unwrap(),
1476 payload: Bytes::from_static(&[0xDE, 0xAD]),
1477 keyframe: true,
1478 duration: None,
1479 };
1480
1481 let err = encode_fragment(info(1, timescale, 0), &[frame]).unwrap_err();
1482 assert!(matches!(err, Error::PtsOverflow));
1483 }
1484
1485 #[test]
1486 fn reordered_pts_round_trips_with_cts() {
1487 let timescale = moq_net::Timescale::new(1_000_000).unwrap();
1488 let input = vec![
1489 Frame {
1490 timestamp: ts(0),
1491 payload: Bytes::from_static(&[0x00]),
1492 keyframe: true,
1493 duration: Some(ts(33_000)),
1494 },
1495 Frame {
1496 timestamp: ts(99_000),
1497 payload: Bytes::from_static(&[0x01]),
1498 keyframe: false,
1499 duration: Some(ts(33_000)),
1500 },
1501 Frame {
1502 timestamp: ts(33_000),
1503 payload: Bytes::from_static(&[0x02]),
1504 keyframe: false,
1505 duration: Some(ts(33_000)),
1506 },
1507 ];
1508
1509 let fragment = encode_fragment(info(1, timescale, 0), &input).unwrap();
1510 let frames = decode(fragment, timescale, Kind::Video).unwrap();
1511
1512 assert_eq!(frames.len(), input.len());
1513 for (actual, expected) in frames.iter().zip(&input) {
1514 assert_eq!(actual.timestamp, expected.timestamp);
1515 assert_eq!(actual.duration, expected.duration);
1516 assert_eq!(actual.payload, expected.payload);
1517 }
1518 }
1519
1520 #[test]
1524 fn audio_samples_decode_without_the_keyframe_bit() {
1525 let timescale = moq_net::Timescale::new(48_000).unwrap();
1526 let input: Vec<Frame> = (0..3)
1527 .map(|i| Frame {
1528 timestamp: Timestamp::new(i * 960, timescale).unwrap(),
1529 payload: Bytes::from_static(&[0x00]),
1530 keyframe: i == 0,
1531 duration: Some(Timestamp::new(960, timescale).unwrap()),
1532 })
1533 .collect();
1534 let fragment = encode_fragment(
1535 FragmentInfo {
1536 kind: Kind::Audio,
1537 ..info(1, timescale, 0)
1538 },
1539 &input,
1540 )
1541 .unwrap();
1542
1543 let flags: Vec<u32> = first_traf(&fragment).trun[0]
1545 .entries
1546 .iter()
1547 .map(|entry| entry.flags.unwrap())
1548 .collect();
1549 assert_eq!(flags, vec![0x0200_0000; 3]);
1550
1551 let audio = decode(fragment.clone(), timescale, Kind::Audio).unwrap();
1552 assert!(
1553 audio.iter().all(|frame| !frame.keyframe),
1554 "audio never decodes a keyframe"
1555 );
1556 let video = decode(fragment, timescale, Kind::Video).unwrap();
1557 assert!(
1558 video.iter().all(|frame| frame.keyframe),
1559 "the sync flag is a video keyframe"
1560 );
1561 }
1562
1563 #[test]
1564 fn unsupported_track_handler_is_rejected() {
1565 assert!(matches!(
1566 Kind::from_handler((*b"text").into()),
1567 Err(Error::UnknownTrackHandler(_))
1568 ));
1569 }
1570
1571 #[test]
1573 fn wire_knows_an_audio_track_by_its_handler() {
1574 let audio = Wire::new(synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap());
1575 assert_eq!(audio.kind().unwrap(), Kind::Audio);
1576
1577 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1578 config.coded_width = Some(320);
1579 config.coded_height = Some(240);
1580 let video = Wire::new(synthesize_video_trak(1, 90_000, &config, None).unwrap());
1581 assert_eq!(video.kind().unwrap(), Kind::Video);
1582 }
1583
1584 #[test]
1585 fn decode_without_duration_reports_none() {
1586 let timescale = moq_net::Timescale::new(90_000).unwrap();
1589 let frames = vec![Frame {
1590 timestamp: ts(0),
1591 payload: Bytes::from_static(&[0xDE, 0xAD]),
1592 keyframe: true,
1593 duration: None,
1594 }];
1595
1596 let fragment = encode_fragment(info(1, timescale, 0), &frames).unwrap();
1597 let frames = decode(fragment, timescale, Kind::Video).unwrap();
1598
1599 assert_eq!(frames.len(), 1);
1600 assert_eq!(frames[0].duration, None);
1601 }
1602
1603 #[test]
1604 fn decode_zero_duration_reports_none() {
1605 use mp4_atom::Encode;
1606
1607 let timescale = moq_net::Timescale::new(24_000).unwrap();
1608 let moof = mp4_atom::Moof {
1609 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
1610 traf: vec![mp4_atom::Traf {
1611 tfhd: mp4_atom::Tfhd {
1612 track_id: 1,
1613 default_sample_duration: Some(0),
1614 default_sample_size: Some(2),
1615 ..Default::default()
1616 },
1617 tfdt: Some(mp4_atom::Tfdt {
1618 base_media_decode_time: 2_000,
1619 }),
1620 trun: vec![mp4_atom::Trun {
1621 data_offset: Some(0),
1622 entries: vec![mp4_atom::TrunEntry {
1623 size: None,
1624 duration: None,
1625 ..Default::default()
1626 }],
1627 }],
1628 ..Default::default()
1629 }],
1630 };
1631
1632 let mut buf = Vec::new();
1633 moof.encode(&mut buf).unwrap();
1634 mp4_atom::Mdat { data: vec![0xDE, 0xAD] }.encode(&mut buf).unwrap();
1635
1636 let frames = decode(Bytes::from(buf), timescale, Kind::Video).unwrap();
1637 assert_eq!(frames.len(), 1);
1638 assert_eq!(frames[0].timestamp.as_micros(), 83_333);
1639 assert_eq!(frames[0].duration, None);
1640 }
1641}