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
20#[cfg(test)]
21mod export_test;
22#[cfg(test)]
23mod import_test;
24
25use std::{task::Poll, time::Duration};
26
27use bytes::Bytes;
28use hang::catalog::{AudioCodec, AudioConfig, VideoCodec, VideoConfig};
29use mp4_atom::Atom;
30
31use moq_net::Timestamp;
32
33use crate::container::{Container, Frame};
34
35#[derive(Debug, Clone, thiserror::Error)]
36#[non_exhaustive]
37pub enum Error {
38 #[error("mp4: {0}")]
39 Mp4(std::sync::Arc<mp4_atom::Error>),
40
41 #[error("moq: {0}")]
42 Moq(#[from] moq_net::Error),
43
44 #[error("flac: {0}")]
45 Flac(#[from] crate::codec::flac::Error),
46
47 #[error("opus: {0}")]
48 Opus(#[from] crate::codec::opus::Error),
49
50 #[error("missing keyframe: a group must open on a keyframe")]
51 MissingKeyframe(#[from] crate::container::MissingKeyframe),
52
53 #[error("timestamp overflow")]
54 TimestampOverflow(#[from] moq_net::TimeOverflow),
55
56 #[error("no traf in moof")]
57 NoTraf,
58
59 #[error("no tfdt in traf")]
60 NoTfdt,
61
62 #[error("PTS overflow")]
63 PtsOverflow,
64
65 #[error("missing moof")]
66 NoMoof,
67
68 #[error("missing mdat")]
69 NoMdat,
70
71 #[error("missing moov")]
72 NoMoov,
73
74 #[error("no tracks in moov")]
75 NoTracks,
76
77 #[error("multiple tracks in moov, use Trak instead")]
78 MultipleTracks,
79
80 #[error("can't synthesize CMAF init for {0}")]
81 UnsupportedSynthesis(String),
82
83 #[error("subtitle tracks are not supported")]
84 UnsupportedSubtitle,
85
86 #[error("unknown track handler: {0:?}")]
87 UnknownTrackHandler([u8; 4]),
88
89 #[error("missing codec")]
90 MissingCodec,
91
92 #[error("multiple codecs")]
93 MultipleCodecs,
94
95 #[error("unknown codec: {0:?}")]
96 UnknownCodec(mp4_atom::FourCC),
97
98 #[error("unsupported codec: {0:?}")]
99 UnsupportedCodec(Box<mp4_atom::Codec>),
100
101 #[error("unsupported codec: MPEG2")]
102 UnsupportedMpeg2,
103
104 #[error("AAC sample entry missing DecoderSpecificInfo")]
106 MissingDecoderSpecific,
107
108 #[error("duplicate moof")]
109 DuplicateMoof,
110
111 #[error("missing trun")]
112 MissingTrun,
113
114 #[error("missing tfdt")]
115 MissingTfdt,
116
117 #[error("video codec {0} needs a description (codec config record) to synthesize a CMAF init")]
118 MissingVideoDescription(String),
119
120 #[error("video track {0} missing in catalog")]
121 MissingVideoTrack(String),
122
123 #[error("missing video dimensions for codec: {0}")]
125 MissingVideoDimensions(String),
126
127 #[error("audio track {0} missing in catalog")]
128 MissingAudioTrack(String),
129
130 #[error("invalid data offset")]
131 InvalidDataOffset,
132
133 #[error("unknown track {0}")]
134 UnknownTrack(u32),
135
136 #[error("no keyframe at start of group")]
137 NoKeyframe,
138
139 #[error("track sample range {start}..{end} is out of bounds of mdat (len {len})")]
140 SampleRangeOutOfBounds { start: usize, end: usize, len: usize },
141
142 #[error("no catalog snapshot")]
143 NoCatalogSnapshot,
144
145 #[error("encode_fragment called with no frames")]
146 NoFrames,
147
148 #[error("audio codec {0} needs a description (AudioSpecificConfig) to synthesize a CMAF init")]
149 MissingAudioDescription(String),
150
151 #[error("multi-sample fragment has a non-final sample with no duration; DTS is unrecoverable")]
152 MissingSampleDuration,
153
154 #[error("timescale {0} does not fit the 32-bit mdhd field")]
157 TimescaleTooLarge(u64),
158
159 #[error("a CMAF rendition's timescale comes from its init segment and can't be overridden")]
162 TimescaleOverride,
163
164 #[error("sample duration {0} does not fit the 32-bit trun field")]
167 SampleDurationTooLarge(u64),
168
169 #[error("sample duration is shorter than one tick at timescale {0}")]
171 SampleDurationTooSmall(u64),
172
173 #[error("sample duration is not exactly representable at timescale {0}")]
175 SampleDurationInexact(u64),
176
177 #[error("duration-less video needs stated durations or presentation-ordered inference")]
179 MissingVideoDuration,
180}
181
182impl From<mp4_atom::Error> for Error {
183 fn from(err: mp4_atom::Error) -> Self {
184 Error::Mp4(std::sync::Arc::new(err))
185 }
186}
187
188pub type Result<T> = std::result::Result<T, Error>;
189
190pub struct Wire {
199 trak: Box<mp4_atom::Trak>,
200}
201
202impl Wire {
203 pub fn new(trak: mp4_atom::Trak) -> Self {
205 Self { trak: Box::new(trak) }
206 }
207
208 pub fn from_init(init_data: &[u8]) -> Result<Self> {
210 use mp4_atom::DecodeMaybe;
211
212 let mut cursor = std::io::Cursor::new(init_data);
213 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
214 if let mp4_atom::Any::Moov(mut moov) = atom {
215 return match moov.trak.len() {
216 1 => Ok(Self::new(moov.trak.remove(0))),
217 0 => Err(Error::NoTracks),
218 _ => Err(Error::MultipleTracks),
219 };
220 }
221 }
222 Err(Error::NoMoov)
223 }
224
225 pub fn trak(&self) -> &mp4_atom::Trak {
226 &self.trak
227 }
228}
229
230impl Container for Wire {
231 type Error = Error;
232
233 fn write(&self, group: &mut moq_net::group::Producer, frames: &[Frame]) -> std::result::Result<(), Self::Error> {
234 let timescale = moq_net::Timescale::new(self.trak.mdia.mdhd.timescale as u64)?;
235 let track_id = self.trak.tkhd.track_id;
236 encode(group, frames, timescale, track_id)
237 }
238
239 fn poll_read(
240 &self,
241 group: &mut moq_net::group::Consumer,
242 waiter: &kio::Waiter,
243 ) -> Poll<std::result::Result<Option<Vec<Frame>>, Self::Error>> {
244 use std::task::ready;
245
246 let Some(frame) = ready!(group.poll_read_frame(waiter)?) else {
247 return Poll::Ready(Ok(None));
248 };
249
250 let timescale = moq_net::Timescale::new(self.trak.mdia.mdhd.timescale as u64)?;
251 Poll::Ready(Ok(Some(decode(frame.payload, timescale)?)))
252 }
253}
254
255pub(crate) fn decode(data: Bytes, timescale: moq_net::Timescale) -> Result<Vec<Frame>> {
256 use mp4_atom::DecodeMaybe;
257
258 let mut cursor = std::io::Cursor::new(&data);
259 let mut moof = None;
260 let mut mdat_data = None;
261
262 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
263 match atom {
264 mp4_atom::Any::Moof(m) => moof = Some(m),
265 mp4_atom::Any::Mdat(m) => mdat_data = Some(m.data),
266 _ => {}
267 }
268 }
269
270 let moof = moof.ok_or(Error::NoMoof)?;
271 let mdat_data = mdat_data.ok_or(Error::NoMdat)?;
272 let traf = moof.traf.first().ok_or(Error::NoTraf)?;
273 let tfdt = traf.tfdt.as_ref().ok_or(Error::NoTfdt)?;
274 let base_dts = tfdt.base_media_decode_time;
275
276 let default_size = traf.tfhd.default_sample_size;
277 let default_duration = traf.tfhd.default_sample_duration;
278
279 let total_samples: usize = traf.trun.iter().map(|t| t.entries.len()).sum();
283
284 let mut frames = Vec::new();
285 let mut offset = 0usize;
286 let mut dts = base_dts;
287 let mut sample_index = 0usize;
288
289 for trun in &traf.trun {
290 for entry in &trun.entries {
291 let size = entry.size.or(default_size).unwrap_or(0) as usize;
292 let end = offset + size;
293
294 if end > mdat_data.len() {
295 return Err(Error::SampleRangeOutOfBounds {
296 start: offset,
297 end,
298 len: mdat_data.len(),
299 });
300 }
301
302 let cts = entry.cts.unwrap_or_default() as i64;
303 let pts = dts.checked_add_signed(cts).ok_or(Error::PtsOverflow)?;
304 let timestamp = Timestamp::new(pts, timescale)?;
306 let payload = Bytes::copy_from_slice(&mdat_data[offset..end]);
307 let flags = entry.flags.unwrap_or(0);
308 let keyframe = (flags >> 24) & 0x3 == 0x2;
310
311 let sample_duration = entry.duration.or(default_duration).filter(|d| *d != 0);
314
315 let is_last = sample_index + 1 == total_samples;
318 if sample_duration.is_none() && !is_last {
319 return Err(Error::MissingSampleDuration);
320 }
321
322 let duration = sample_duration
323 .map(|d| Timestamp::new(d as u64, timescale))
324 .transpose()?;
325
326 frames.push(Frame {
327 timestamp,
328 payload,
329 keyframe,
330 duration,
331 });
332
333 offset = end;
334 dts += sample_duration.unwrap_or(0) as u64;
335 sample_index += 1;
336 }
337 }
338
339 Ok(frames)
340}
341
342pub(crate) fn encode(
343 group: &mut moq_net::group::Producer,
344 frames: &[Frame],
345 timescale: moq_net::Timescale,
346 track_id: u32,
347) -> Result<()> {
348 if frames.is_empty() {
349 return Ok(());
350 }
351
352 let sequence_number = group.frame_count() as u32;
353 let info = FragmentInfo {
354 track_id,
355 timescale,
356 sequence_number,
357 };
358 let bytes = encode_fragment(info, frames)?;
359 let mut writer = group.create_frame(moq_net::frame::Info {
362 size: bytes.len() as u64,
363 timestamp: frames[0].timestamp,
364 })?;
365 writer.write(bytes)?;
366 writer.finish()?;
367
368 Ok(())
369}
370
371#[derive(Debug, Clone, Copy)]
376pub(crate) struct FragmentInfo {
377 pub track_id: u32,
379 pub timescale: moq_net::Timescale,
381 pub sequence_number: u32,
383}
384
385pub(crate) fn encode_fragment(info: FragmentInfo, frames: &[Frame]) -> Result<Bytes> {
393 let Some(first) = frames.first() else {
394 return Ok(Bytes::new());
395 };
396 encode_at(info, base_ticks(first, info.timescale)?, frames)
397}
398
399fn base_ticks(frame: &Frame, timescale: moq_net::Timescale) -> Result<u64> {
405 timestamp_ticks(frame.timestamp, timescale)
406}
407
408fn timestamp_ticks(timestamp: Timestamp, timescale: moq_net::Timescale) -> Result<u64> {
410 let source_scale = u128::from(timestamp.scale().as_u64());
411 let scaled = u128::from(timestamp.value()) * u128::from(timescale.as_u64());
412 let ticks = (scaled + source_scale / 2) / source_scale;
413 u64::try_from(ticks).map_err(|_| Error::PtsOverflow)
414}
415
416fn encode_at(info: FragmentInfo, base_dts: u64, frames: &[Frame]) -> Result<Bytes> {
433 let FragmentInfo {
434 track_id,
435 timescale,
436 sequence_number,
437 } = info;
438
439 use mp4_atom::Encode;
440
441 if frames.is_empty() {
442 return Ok(Bytes::new());
443 }
444
445 let mut dts = base_dts;
446
447 let entries: Vec<_> = frames
448 .iter()
449 .map(|f| {
450 let flags = if f.keyframe { 0x0200_0000 } else { 0x0001_0000 };
451 let duration = f.duration.map(|d| trun_duration(d, timescale)).transpose()?;
454 let pts = i128::from(timestamp_ticks(f.timestamp, timescale)?);
455 let cts = pts - i128::from(dts);
456 let cts = i32::try_from(cts).map_err(|_| Error::PtsOverflow)?;
457
458 if let Some(duration) = duration {
461 dts = dts.checked_add(u64::from(duration)).ok_or(Error::PtsOverflow)?;
462 }
463
464 Ok(mp4_atom::TrunEntry {
465 duration,
466 size: Some(f.payload.len() as u32),
467 flags: Some(flags),
468 cts: (cts != 0).then_some(cts),
469 })
470 })
471 .collect::<Result<_>>()?;
472
473 let mdat_data: Vec<u8> = frames.iter().flat_map(|f| f.payload.iter().copied()).collect();
474
475 let build_moof = |data_offset| mp4_atom::Moof {
476 mfhd: mp4_atom::Mfhd { sequence_number },
477 traf: vec![mp4_atom::Traf {
478 tfhd: mp4_atom::Tfhd {
479 track_id,
480 ..Default::default()
481 },
482 tfdt: Some(mp4_atom::Tfdt {
483 base_media_decode_time: base_dts,
484 }),
485 trun: vec![mp4_atom::Trun {
486 data_offset: Some(data_offset),
487 entries: entries.clone(),
488 }],
489 ..Default::default()
490 }],
491 };
492
493 let mut buf = Vec::new();
495 build_moof(0).encode(&mut buf)?;
496 let moof_size = buf.len();
497
498 buf.clear();
500 build_moof((moof_size + 8) as i32).encode(&mut buf)?;
501
502 let mdat = mp4_atom::Mdat { data: mdat_data };
503 mdat.encode(&mut buf)?;
504
505 Ok(Bytes::from(buf))
506}
507
508fn trun_duration(duration: Timestamp, timescale: moq_net::Timescale) -> Result<u32> {
510 let source_timescale = duration.scale().as_u64();
511 let output_timescale = timescale.as_u64();
512 let scaled = u128::from(duration.value()) * u128::from(output_timescale);
513 let ticks = scaled / u128::from(source_timescale);
514 if !duration.is_zero() && ticks == 0 {
515 return Err(Error::SampleDurationTooSmall(timescale.as_u64()));
516 }
517 if scaled % u128::from(source_timescale) != 0 {
518 return Err(Error::SampleDurationInexact(output_timescale));
519 }
520 u32::try_from(ticks).map_err(|_| Error::SampleDurationTooLarge(u64::try_from(ticks).unwrap_or(u64::MAX)))
521}
522
523pub(crate) fn synthesize_video_trak(
531 track_id: u32,
532 timescale: u64,
533 config: &VideoConfig,
534 description: Option<&[u8]>,
535) -> Result<mp4_atom::Trak> {
536 if !matches!(
537 config.codec,
538 VideoCodec::H264(_) | VideoCodec::H265(_) | VideoCodec::AV1(_) | VideoCodec::VP8 | VideoCodec::VP9(_)
539 ) {
540 return Err(Error::UnsupportedSynthesis(format!("video codec {:?}", config.codec)));
541 }
542
543 let width = u16::try_from(
544 config
545 .coded_width
546 .ok_or_else(|| Error::MissingVideoDimensions(config.codec.to_string()))?,
547 )
548 .map_err(|_| Error::MissingVideoDimensions(config.codec.to_string()))?;
549 let height = u16::try_from(
550 config
551 .coded_height
552 .ok_or_else(|| Error::MissingVideoDimensions(config.codec.to_string()))?,
553 )
554 .map_err(|_| Error::MissingVideoDimensions(config.codec.to_string()))?;
555 if width == 0 || height == 0 {
556 return Err(Error::MissingVideoDimensions(config.codec.to_string()));
557 }
558 let visual = mp4_atom::Visual {
559 data_reference_index: 1,
560 width,
561 height,
562 ..Default::default()
563 };
564
565 let require_description = || description.ok_or_else(|| Error::MissingVideoDescription(config.codec.to_string()));
567
568 let sample_entry = match &config.codec {
569 VideoCodec::H264(_) => {
570 let mut cursor = std::io::Cursor::new(require_description()?);
571 let avcc = mp4_atom::Avcc::decode_body(&mut cursor).map_err(Error::from)?;
572 mp4_atom::Codec::from(mp4_atom::Avc1 {
573 visual,
574 avcc,
575 ..Default::default()
576 })
577 }
578 VideoCodec::H265(h265) => {
579 let mut cursor = std::io::Cursor::new(require_description()?);
580 let hvcc = mp4_atom::Hvcc::decode_body(&mut cursor).map_err(Error::from)?;
581 if h265.in_band {
583 mp4_atom::Codec::from(mp4_atom::Hev1 {
584 visual,
585 hvcc,
586 ..Default::default()
587 })
588 } else {
589 mp4_atom::Codec::from(mp4_atom::Hvc1 {
590 visual,
591 hvcc,
592 ..Default::default()
593 })
594 }
595 }
596 VideoCodec::AV1(av1) => mp4_atom::Codec::from(mp4_atom::Av01 {
597 visual,
598 av1c: crate::codec::av1::av1c_from_av1(av1),
599 ..Default::default()
600 }),
601 VideoCodec::VP8 => mp4_atom::Codec::from(mp4_atom::Vp08 {
602 visual,
603 vpcc: crate::codec::vp8::vpcc(),
604 ..Default::default()
605 }),
606 VideoCodec::VP9(vp9) => mp4_atom::Codec::from(mp4_atom::Vp09 {
607 visual,
608 vpcc: crate::codec::vp9::vpcc(vp9),
609 ..Default::default()
610 }),
611 other => unreachable!("unsupported codecs rejected before geometry synthesis: {other:?}"),
612 };
613
614 Ok(build_video_trak(
615 track_id,
616 mdhd_timescale(timescale)?,
617 sample_entry,
618 width,
619 height,
620 ))
621}
622
623pub(crate) fn synthesize_audio_trak(track_id: u32, timescale: u64, config: &AudioConfig) -> Result<mp4_atom::Trak> {
625 use mp4_atom::Decode;
626
627 let audio = mp4_atom::Audio {
628 data_reference_index: 1,
629 channel_count: config.channel_count as u16,
630 sample_size: 16,
631 sample_rate: mp4_atom::FixedPoint::from(config.sample_rate as u16),
632 };
633
634 let sample_entry = match &config.codec {
635 AudioCodec::Opus => {
636 let pre_skip = match &config.description {
637 Some(description) => {
638 let mut description = description.as_ref();
639 crate::codec::opus::Config::parse(&mut description)?.pre_skip
640 }
641 None => 0,
642 };
643 mp4_atom::Codec::from(mp4_atom::Opus {
644 audio,
645 dops: mp4_atom::Dops {
646 output_channel_count: config.channel_count as u8,
647 pre_skip,
648 input_sample_rate: config.sample_rate,
649 output_gain: 0,
650 },
651 btrt: None,
652 })
653 }
654 AudioCodec::AAC(_) => {
655 let description = config
660 .description
661 .as_ref()
662 .ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
663 let mut cursor = std::io::Cursor::new(description.as_ref());
664 let dec_specific = mp4_atom::esds::DecoderSpecific::decode(&mut cursor)?;
665
666 let bitrate = config.bitrate.and_then(|b| u32::try_from(b).ok()).filter(|b| *b > 0);
674 let (max_bitrate, avg_bitrate) = match bitrate {
675 Some(bitrate) => (bitrate, bitrate),
676 None => (256_000, 128_000),
677 };
678 mp4_atom::Codec::from(mp4_atom::Mp4a {
679 audio,
680 esds: mp4_atom::Esds {
681 es_desc: mp4_atom::esds::EsDescriptor {
682 es_id: 0,
684 dec_config: mp4_atom::esds::DecoderConfig {
685 object_type_indication: 0x40, stream_type: 0x05, up_stream: 0,
688 buffer_size_db: mp4_atom::u24::from([0x00, 0x60, 0x00]),
690 max_bitrate,
691 avg_bitrate,
692 dec_specific: Some(dec_specific),
693 },
694 sl_config: Default::default(),
695 },
696 },
697 btrt: None,
698 taic: None,
699 })
700 }
701 AudioCodec::Flac => {
702 let description = config
705 .description
706 .as_ref()
707 .ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
708 let info = crate::codec::flac::Config::parse(&mut description.as_ref())?;
709
710 let stream_info = mp4_atom::FlacMetadataBlock::StreamInfo {
711 minimum_block_size: info.min_block_size,
712 maximum_block_size: info.max_block_size,
713 minimum_frame_size: info.min_frame_size.min(0xFF_FFFF).try_into().expect("fits in u24"),
715 maximum_frame_size: info.max_frame_size.min(0xFF_FFFF).try_into().expect("fits in u24"),
716 sample_rate: info.sample_rate,
717 num_channels_minus_one: info.channel_count.saturating_sub(1) as u8,
718 bits_per_sample_minus_one: info.bits_per_sample.saturating_sub(1) as u8,
719 number_of_interchannel_samples: info.total_samples,
720 md5_checksum: info.md5.to_vec(),
721 };
722
723 mp4_atom::Codec::from(mp4_atom::Flac {
724 audio,
725 dfla: mp4_atom::Dfla {
726 blocks: vec![stream_info],
727 },
728 })
729 }
730 other => return Err(Error::UnsupportedSynthesis(format!("audio codec {:?}", other))),
731 };
732
733 Ok(build_audio_trak(track_id, mdhd_timescale(timescale)?, sample_entry))
734}
735
736const UNKNOWN_DURATION: u64 = u64::MAX;
740
741fn build_video_trak(
742 track_id: u32,
743 timescale: u32,
744 sample_entry: mp4_atom::Codec,
745 width: u16,
746 height: u16,
747) -> mp4_atom::Trak {
748 mp4_atom::Trak {
749 tkhd: mp4_atom::Tkhd {
750 track_id,
751 enabled: true,
752 in_movie: true,
755 duration: UNKNOWN_DURATION,
756 width: mp4_atom::FixedPoint::from(width),
757 height: mp4_atom::FixedPoint::from(height),
758 ..Default::default()
759 },
760 mdia: build_mdia(timescale, b"vide", true, sample_entry),
761 ..Default::default()
762 }
763}
764
765fn build_audio_trak(track_id: u32, timescale: u32, sample_entry: mp4_atom::Codec) -> mp4_atom::Trak {
766 mp4_atom::Trak {
767 tkhd: mp4_atom::Tkhd {
768 track_id,
769 enabled: true,
770 in_movie: true,
771 duration: UNKNOWN_DURATION,
772 volume: mp4_atom::FixedPoint::from(1),
775 ..Default::default()
776 },
777 mdia: build_mdia(timescale, b"soun", false, sample_entry),
778 ..Default::default()
779 }
780}
781
782pub(crate) fn encode_init(
789 ftyp: Option<mp4_atom::Ftyp>,
790 traks: Vec<mp4_atom::Trak>,
791 trexs: Vec<mp4_atom::Trex>,
792) -> Result<Bytes> {
793 use mp4_atom::Encode;
794
795 let ftyp = ftyp.unwrap_or(mp4_atom::Ftyp {
796 major_brand: b"isom".into(),
797 minor_version: 0x200,
798 compatible_brands: vec![b"isom".into(), b"iso6".into(), b"mp41".into()],
799 });
800 let timescale = traks.first().map(|t| t.mdia.mdhd.timescale).unwrap_or(1000);
801 let next_track_id = traks.iter().map(|t| t.tkhd.track_id).max().unwrap_or(0) + 1;
802
803 let moov = mp4_atom::Moov {
804 mvhd: mp4_atom::Mvhd {
805 timescale,
806 duration: UNKNOWN_DURATION,
807 rate: mp4_atom::FixedPoint::from(1),
810 volume: mp4_atom::FixedPoint::from(1),
811 next_track_id,
813 ..Default::default()
814 },
815 trak: traks,
816 mvex: (!trexs.is_empty()).then(|| mp4_atom::Mvex {
817 trex: trexs,
818 ..Default::default()
819 }),
820 ..Default::default()
821 };
822
823 let mut buf = Vec::new();
824 ftyp.encode(&mut buf)?;
825 moov.encode(&mut buf)?;
826 Ok(Bytes::from(buf))
827}
828
829fn mdhd_timescale(timescale: u64) -> Result<u32> {
835 u32::try_from(timescale).map_err(|_| Error::TimescaleTooLarge(timescale))
836}
837
838fn build_mdia(timescale: u32, handler: &[u8; 4], is_video: bool, sample_entry: mp4_atom::Codec) -> mp4_atom::Mdia {
839 mp4_atom::Mdia {
840 mdhd: mp4_atom::Mdhd {
841 timescale,
842 ..Default::default()
843 },
844 hdlr: mp4_atom::Hdlr {
845 handler: mp4_atom::FourCC::new(handler),
846 name: String::new(),
847 },
848 minf: mp4_atom::Minf {
849 vmhd: is_video.then(mp4_atom::Vmhd::default),
850 smhd: (!is_video).then(mp4_atom::Smhd::default),
851 dinf: mp4_atom::Dinf {
852 dref: mp4_atom::Dref {
853 urls: vec![mp4_atom::Url::default()],
854 },
855 },
856 stbl: mp4_atom::Stbl {
857 stsd: mp4_atom::Stsd {
858 codecs: vec![sample_entry],
859 },
860 ..Default::default()
861 },
862 ..Default::default()
863 },
864 }
865}
866
867pub(crate) fn default_video_timescale(config: &VideoConfig) -> u64 {
878 usable_video_framerate(config)
879 .and_then(select_video_timescale)
880 .unwrap_or(90_000)
881}
882
883pub(crate) fn usable_video_framerate(config: &VideoConfig) -> Option<f64> {
885 config
886 .framerate
887 .filter(|fps| fps.is_finite() && *fps > 0.0 && (*fps * 1000.0) as u64 > 0)
888 .filter(|fps| select_video_timescale(*fps).is_some())
889}
890
891fn select_video_timescale(framerate: f64) -> Option<u64> {
893 let preferred = (framerate * 1000.0) as u64;
894
895 let frame = Duration::from_secs_f64(1.0 / framerate);
896 for timescale in [preferred, (framerate * 1001.0).round() as u64] {
897 if duration_fits_trun(frame, timescale) {
898 return Some(timescale);
899 }
900 }
901
902 const NANOS_PER_SECOND: u128 = 1_000_000_000;
903 let exact = NANOS_PER_SECOND / gcd(frame.as_nanos(), NANOS_PER_SECOND);
904 let exact = u64::try_from(exact).ok()?;
905 if duration_fits_trun(frame, exact) {
906 return Some(exact);
907 }
908 if let Some(timescale) = rational_timescale(frame) {
909 return Some(timescale);
910 }
911
912 let max_scale = u128::from(u32::MAX)
913 .checked_mul(NANOS_PER_SECOND)?
914 .checked_div(frame.as_nanos())?
915 .min(u128::from(u32::MAX));
916 let max_scale = u64::try_from(max_scale).ok()?;
917 duration_fits_trun(frame, max_scale).then_some(max_scale)
918}
919
920fn rational_timescale(duration: Duration) -> Option<u64> {
922 const NANOS_PER_SECOND: u128 = 1_000_000_000;
923 let mut numerator = duration.as_nanos();
924 let mut denominator = NANOS_PER_SECOND;
925 let (mut previous_ticks, mut ticks) = (0_u128, 1_u128);
926 let (mut previous_scale, mut scale) = (1_u128, 0_u128);
927
928 while denominator != 0 {
929 let coefficient = numerator / denominator;
930 let next_ticks = coefficient.checked_mul(ticks)?.checked_add(previous_ticks)?;
931 let next_scale = coefficient.checked_mul(scale)?.checked_add(previous_scale)?;
932 if next_ticks > u128::from(u32::MAX) || next_scale > u128::from(u32::MAX) {
933 break;
934 }
935
936 let candidate = u64::try_from(next_scale).ok()?;
937 if duration_fits_trun(duration, candidate) {
938 return Some(candidate);
939 }
940
941 (previous_ticks, ticks) = (ticks, next_ticks);
942 (previous_scale, scale) = (scale, next_scale);
943 (numerator, denominator) = (denominator, numerator % denominator);
944 }
945
946 None
947}
948
949fn duration_fits_trun(duration: Duration, timescale: u64) -> bool {
951 timescale > 0 && rounded_duration_ticks(duration, timescale).is_some_and(|ticks| u32::try_from(ticks).is_ok())
952}
953
954fn rounded_duration_ticks(duration: Duration, timescale: u64) -> Option<u64> {
956 const NANOS_PER_SECOND: u128 = 1_000_000_000;
957 let scaled = duration.as_nanos().checked_mul(u128::from(timescale))?;
958 let rounded = scaled.checked_add(NANOS_PER_SECOND / 2)? / NANOS_PER_SECOND;
959 let exact = rounded.checked_mul(NANOS_PER_SECOND)?;
960 if scaled.abs_diff(exact) > u128::from(timescale) {
961 return None;
962 }
963 u64::try_from(rounded).ok()
964}
965
966fn gcd(mut a: u128, mut b: u128) -> u128 {
968 while b != 0 {
969 (a, b) = (b, a % b);
970 }
971 a
972}
973
974#[cfg(test)]
977pub(crate) fn timeline(fragment: &Bytes) -> (u64, Vec<i32>) {
978 let traf = first_traf(fragment);
979 let cts = traf.trun[0].entries.iter().map(|e| e.cts.unwrap_or_default()).collect();
980 (traf.tfdt.as_ref().unwrap().base_media_decode_time, cts)
981}
982
983#[cfg(test)]
985pub(crate) fn sample_durations(fragment: &Bytes) -> Vec<Option<u32>> {
986 first_traf(fragment).trun[0]
987 .entries
988 .iter()
989 .map(|e| e.duration)
990 .collect()
991}
992
993#[cfg(test)]
994fn first_traf(fragment: &Bytes) -> mp4_atom::Traf {
995 use mp4_atom::DecodeMaybe;
996
997 let mut cursor = std::io::Cursor::new(fragment.as_ref());
998 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
999 if let mp4_atom::Any::Moof(moof) = atom {
1000 return moof.traf.into_iter().next().expect("a traf");
1001 }
1002 }
1003 panic!("no moof");
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009
1010 fn ts(micros: u64) -> Timestamp {
1011 Timestamp::from_micros(micros).unwrap()
1012 }
1013
1014 fn info(track_id: u32, timescale: moq_net::Timescale, sequence_number: u32) -> FragmentInfo {
1015 FragmentInfo {
1016 track_id,
1017 timescale,
1018 sequence_number,
1019 }
1020 }
1021
1022 fn aac_config(bitrate: Option<u64>) -> AudioConfig {
1024 let mut config = AudioConfig::new(AudioCodec::AAC(hang::catalog::AAC { profile: 2 }), 44_100, 2);
1025 config.description = Some(Bytes::from_static(&[0x12, 0x10]));
1026 config.bitrate = bitrate;
1027 config
1028 }
1029
1030 fn moov(init: &Bytes) -> mp4_atom::Moov {
1033 use mp4_atom::DecodeMaybe;
1034
1035 let mut cursor = std::io::Cursor::new(init.as_ref());
1036 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
1037 if let mp4_atom::Any::Moov(moov) = atom {
1038 return moov;
1039 }
1040 }
1041 panic!("no moov");
1042 }
1043
1044 fn dec_config(trak: &mp4_atom::Trak) -> mp4_atom::esds::DecoderConfig {
1045 match &trak.mdia.minf.stbl.stsd.codecs[0] {
1046 mp4_atom::Codec::Mp4a(mp4a) => mp4a.esds.es_desc.dec_config.clone(),
1047 other => panic!("expected mp4a, got {other:?}"),
1048 }
1049 }
1050
1051 #[test]
1055 fn synthesized_aac_init_has_non_zero_bitrates() {
1056 let inferred = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap());
1057 assert_ne!(u32::from(inferred.buffer_size_db), 0);
1058 assert_ne!(inferred.max_bitrate, 0);
1059 assert_ne!(inferred.avg_bitrate, 0);
1060
1061 let stated = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(Some(96_000))).unwrap());
1062 assert_eq!(stated.max_bitrate, 96_000, "the catalog's bitrate wins");
1063 assert_eq!(stated.avg_bitrate, 96_000);
1064
1065 for unusable in [Some(0), Some(u64::from(u32::MAX) + 1)] {
1068 let config = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(unusable)).unwrap());
1069 assert_eq!(config.max_bitrate, inferred.max_bitrate, "{unusable:?}");
1070 assert_eq!(config.avg_bitrate, inferred.avg_bitrate, "{unusable:?}");
1071 }
1072 }
1073
1074 #[test]
1077 fn default_video_timescale_ignores_an_unusable_framerate() {
1078 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1079 for unusable in [0.0, -30.0, f64::NAN, f64::INFINITY, 0.0005] {
1080 config.framerate = Some(unusable);
1081 assert_eq!(default_video_timescale(&config), 90_000, "{unusable}");
1082 }
1083
1084 config.framerate = Some(30.0);
1085 assert_eq!(default_video_timescale(&config), 30_000);
1086
1087 config.framerate = Some(30_000.0 / 1001.0);
1088 assert_eq!(default_video_timescale(&config), 30_000);
1089 }
1090
1091 #[test]
1092 fn default_video_timescale_keeps_low_cadence_within_trun() {
1093 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1094 for framerate in [0.2001, 0.0011] {
1095 config.framerate = Some(framerate);
1096 let timescale = default_video_timescale(&config);
1097 let duration = Duration::from_secs_f64(1.0 / framerate);
1098 let ticks = rounded_duration_ticks(duration, timescale).unwrap();
1099
1100 assert!(timescale <= u64::from(u32::MAX));
1101 assert!(ticks <= u64::from(u32::MAX));
1102 }
1103
1104 assert_eq!(default_video_timescale(&config), 11);
1105 }
1106
1107 #[test]
1110 fn synthesized_init_declares_unknown_duration() {
1111 let trak = synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap();
1112 assert_eq!(trak.tkhd.duration, u64::MAX);
1113
1114 let init = encode_init(None, vec![trak], Vec::new()).unwrap();
1115 let moov = moov(&init);
1116 assert_eq!(moov.mvhd.duration, u64::MAX);
1117 assert_eq!(moov.trak[0].tkhd.duration, u64::MAX);
1118 }
1119
1120 #[test]
1124 fn synthesized_init_headers_describe_a_playable_presentation() {
1125 let audio = synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap();
1126 let init = encode_init(None, vec![audio], Vec::new()).unwrap();
1127 let moov = moov(&init);
1128
1129 assert_eq!(moov.mvhd.rate.integer(), 1, "normal playback rate");
1130 assert_eq!(moov.mvhd.volume.integer(), 1, "full volume");
1131 assert_eq!(moov.mvhd.next_track_id, 2, "past the only track id");
1132
1133 let tkhd = &moov.trak[0].tkhd;
1134 assert!(tkhd.enabled && tkhd.in_movie, "flags 0x000003");
1135 assert_eq!(tkhd.volume.integer(), 1, "an audio track carries the volume");
1136 }
1137
1138 #[test]
1140 fn synthesized_video_init_sets_the_track_flags() {
1141 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1142 config.coded_width = Some(320);
1143 config.coded_height = Some(240);
1144 config.framerate = Some(30.0);
1145 let video = synthesize_video_trak(1, 30_000, &config, None).unwrap();
1146 let init = encode_init(None, vec![video], Vec::new()).unwrap();
1147 let moov = moov(&init);
1148
1149 let tkhd = &moov.trak[0].tkhd;
1150 assert!(tkhd.enabled && tkhd.in_movie, "flags 0x000003");
1151 assert_eq!(tkhd.volume.integer(), 0);
1152 }
1153
1154 #[test]
1155 fn synthesized_video_init_rejects_missing_dimensions() {
1156 let config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1157 let error = synthesize_video_trak(1, 30_000, &config, None).unwrap_err();
1158 assert!(matches!(error, Error::MissingVideoDimensions(_)));
1159 }
1160
1161 #[test]
1162 fn decode_reads_trun_sample_duration() {
1163 use mp4_atom::Encode;
1164
1165 let timescale = moq_net::Timescale::MICRO;
1169 let moof = mp4_atom::Moof {
1170 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
1171 traf: vec![mp4_atom::Traf {
1172 tfhd: mp4_atom::Tfhd {
1173 track_id: 1,
1174 ..Default::default()
1175 },
1176 tfdt: Some(mp4_atom::Tfdt {
1177 base_media_decode_time: 0,
1178 }),
1179 trun: vec![mp4_atom::Trun {
1180 data_offset: Some(0),
1181 entries: vec![
1182 mp4_atom::TrunEntry {
1183 size: Some(2),
1184 duration: Some(33_333),
1185 ..Default::default()
1186 },
1187 mp4_atom::TrunEntry {
1188 size: Some(2),
1189 duration: Some(33_333),
1190 ..Default::default()
1191 },
1192 ],
1193 }],
1194 ..Default::default()
1195 }],
1196 };
1197
1198 let mut buf = Vec::new();
1199 moof.encode(&mut buf).unwrap();
1200 mp4_atom::Mdat {
1201 data: vec![0xDE, 0xAD, 0xBE, 0xEF],
1202 }
1203 .encode(&mut buf)
1204 .unwrap();
1205
1206 let frames = decode(Bytes::from(buf), timescale).unwrap();
1207 assert_eq!(frames.len(), 2);
1208 assert_eq!(frames[0].timestamp, ts(0));
1209 assert_eq!(frames[0].duration, Some(ts(33_333)));
1210 assert_eq!(frames[1].timestamp, ts(33_333));
1211 assert_eq!(frames[1].duration, Some(ts(33_333)));
1212 }
1213
1214 #[test]
1215 fn duration_round_trips_through_encode() {
1216 let timescale = moq_net::Timescale::MICRO;
1218 let input = vec![Frame {
1219 timestamp: ts(0),
1220 payload: Bytes::from_static(&[0xDE, 0xAD]),
1221 keyframe: true,
1222 duration: Some(ts(33_333)),
1223 }];
1224
1225 let fragment = encode_fragment(info(1, timescale, 0), &input).unwrap();
1226 let frames = decode(fragment, timescale).unwrap();
1227
1228 assert_eq!(frames.len(), 1);
1229 assert_eq!(frames[0].duration, Some(ts(33_333)));
1230 }
1231
1232 #[test]
1235 fn encode_fragment_rejects_a_duration_too_large_for_trun() {
1236 let timescale = moq_net::Timescale::new(u64::from(u32::MAX)).unwrap();
1237 let over = u64::from(u32::MAX) + 1;
1238 let frame = Frame {
1239 timestamp: Timestamp::from_scale(0, timescale.as_u64()).unwrap(),
1240 payload: Bytes::from_static(&[0xDE, 0xAD]),
1241 keyframe: true,
1242 duration: Some(Timestamp::from_scale(over, timescale.as_u64()).unwrap()),
1243 };
1244
1245 let err = encode_fragment(info(1, timescale, 0), std::slice::from_ref(&frame)).unwrap_err();
1246 assert!(matches!(err, Error::SampleDurationTooLarge(ticks) if ticks == over));
1247
1248 let largest = Frame {
1249 duration: Some(Timestamp::from_scale(u64::from(u32::MAX), timescale.as_u64()).unwrap()),
1250 ..frame
1251 };
1252 let fragment = encode_fragment(info(1, timescale, 0), &[largest]).unwrap();
1253 assert_eq!(sample_durations(&fragment), vec![Some(u32::MAX)]);
1254 }
1255
1256 #[test]
1259 fn encode_fragment_rejects_a_duration_shorter_than_one_tick() {
1260 let timescale = moq_net::Timescale::SECOND;
1261 let frame = Frame {
1262 timestamp: Timestamp::from_secs(0).unwrap(),
1263 payload: Bytes::from_static(&[0xDE, 0xAD]),
1264 keyframe: true,
1265 duration: Some(Timestamp::from_millis(33).unwrap()),
1266 };
1267
1268 let err = encode_fragment(info(1, timescale, 0), std::slice::from_ref(&frame)).unwrap_err();
1269 assert!(matches!(err, Error::SampleDurationTooSmall(1)));
1270
1271 let one_tick = Frame {
1272 duration: Some(Timestamp::from_secs(1).unwrap()),
1273 ..frame
1274 };
1275 let fragment = encode_fragment(info(1, timescale, 0), &[one_tick]).unwrap();
1276 assert_eq!(sample_durations(&fragment), vec![Some(1)]);
1277 }
1278
1279 #[test]
1282 fn encode_fragment_rejects_an_inexact_sample_duration() {
1283 let input_scale = moq_net::Timescale::new(24).unwrap();
1284 let output_scale = moq_net::Timescale::MILLI;
1285 let frame = Frame {
1286 timestamp: Timestamp::new(0, input_scale).unwrap(),
1287 payload: Bytes::from_static(&[0xDE, 0xAD]),
1288 keyframe: true,
1289 duration: Some(Timestamp::new(1, input_scale).unwrap()),
1290 };
1291
1292 let err = encode_fragment(info(1, output_scale, 0), std::slice::from_ref(&frame)).unwrap_err();
1293 assert!(matches!(err, Error::SampleDurationInexact(1_000)));
1294
1295 let exact_scale = moq_net::Timescale::new(24_000).unwrap();
1296 let fragment = encode_fragment(info(1, exact_scale, 0), &[frame]).unwrap();
1297 assert_eq!(sample_durations(&fragment), vec![Some(1_000)]);
1298 }
1299
1300 #[test]
1303 fn encode_fragment_rejects_a_pts_too_large_for_tfdt() {
1304 let timescale = moq_net::Timescale::new(u64::from(u32::MAX)).unwrap();
1305 let frame = Frame {
1306 timestamp: Timestamp::from_secs(1 << 40).unwrap(),
1307 payload: Bytes::from_static(&[0xDE, 0xAD]),
1308 keyframe: true,
1309 duration: None,
1310 };
1311
1312 let err = encode_fragment(info(1, timescale, 0), &[frame]).unwrap_err();
1313 assert!(matches!(err, Error::PtsOverflow));
1314 }
1315
1316 #[test]
1317 fn reordered_pts_round_trips_with_cts() {
1318 let timescale = moq_net::Timescale::new(1_000_000).unwrap();
1319 let input = vec![
1320 Frame {
1321 timestamp: ts(0),
1322 payload: Bytes::from_static(&[0x00]),
1323 keyframe: true,
1324 duration: Some(ts(33_000)),
1325 },
1326 Frame {
1327 timestamp: ts(99_000),
1328 payload: Bytes::from_static(&[0x01]),
1329 keyframe: false,
1330 duration: Some(ts(33_000)),
1331 },
1332 Frame {
1333 timestamp: ts(33_000),
1334 payload: Bytes::from_static(&[0x02]),
1335 keyframe: false,
1336 duration: Some(ts(33_000)),
1337 },
1338 ];
1339
1340 let fragment = encode_fragment(info(1, timescale, 0), &input).unwrap();
1341 let frames = decode(fragment, timescale).unwrap();
1342
1343 assert_eq!(frames.len(), input.len());
1344 for (actual, expected) in frames.iter().zip(&input) {
1345 assert_eq!(actual.timestamp, expected.timestamp);
1346 assert_eq!(actual.duration, expected.duration);
1347 assert_eq!(actual.payload, expected.payload);
1348 }
1349 }
1350
1351 #[test]
1352 fn decode_without_duration_reports_none() {
1353 let timescale = moq_net::Timescale::new(90_000).unwrap();
1356 let frames = vec![Frame {
1357 timestamp: ts(0),
1358 payload: Bytes::from_static(&[0xDE, 0xAD]),
1359 keyframe: true,
1360 duration: None,
1361 }];
1362
1363 let fragment = encode_fragment(info(1, timescale, 0), &frames).unwrap();
1364 let frames = decode(fragment, timescale).unwrap();
1365
1366 assert_eq!(frames.len(), 1);
1367 assert_eq!(frames[0].duration, None);
1368 }
1369
1370 #[test]
1371 fn decode_zero_duration_reports_none() {
1372 use mp4_atom::Encode;
1373
1374 let timescale = moq_net::Timescale::new(24_000).unwrap();
1375 let moof = mp4_atom::Moof {
1376 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
1377 traf: vec![mp4_atom::Traf {
1378 tfhd: mp4_atom::Tfhd {
1379 track_id: 1,
1380 default_sample_duration: Some(0),
1381 default_sample_size: Some(2),
1382 ..Default::default()
1383 },
1384 tfdt: Some(mp4_atom::Tfdt {
1385 base_media_decode_time: 2_000,
1386 }),
1387 trun: vec![mp4_atom::Trun {
1388 data_offset: Some(0),
1389 entries: vec![mp4_atom::TrunEntry {
1390 size: None,
1391 duration: None,
1392 ..Default::default()
1393 }],
1394 }],
1395 ..Default::default()
1396 }],
1397 };
1398
1399 let mut buf = Vec::new();
1400 moof.encode(&mut buf).unwrap();
1401 mp4_atom::Mdat { data: vec![0xDE, 0xAD] }.encode(&mut buf).unwrap();
1402
1403 let frames = decode(Bytes::from(buf), timescale).unwrap();
1404 assert_eq!(frames.len(), 1);
1405 assert_eq!(frames[0].timestamp.as_micros(), 83_333);
1406 assert_eq!(frames[0].duration, None);
1407 }
1408}