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("audio track {0} missing in catalog")]
124 MissingAudioTrack(String),
125
126 #[error("invalid data offset")]
127 InvalidDataOffset,
128
129 #[error("unknown track {0}")]
130 UnknownTrack(u32),
131
132 #[error("no keyframe at start of group")]
133 NoKeyframe,
134
135 #[error("track sample range {start}..{end} is out of bounds of mdat (len {len})")]
136 SampleRangeOutOfBounds { start: usize, end: usize, len: usize },
137
138 #[error("no catalog snapshot")]
139 NoCatalogSnapshot,
140
141 #[error("encode_fragment called with no frames")]
142 NoFrames,
143
144 #[error("audio codec {0} needs a description (AudioSpecificConfig) to synthesize a CMAF init")]
145 MissingAudioDescription(String),
146
147 #[error("multi-sample fragment has a non-final sample with no duration; DTS is unrecoverable")]
148 MissingSampleDuration,
149
150 #[error("timescale {0} does not fit the 32-bit mdhd field")]
153 TimescaleTooLarge(u64),
154
155 #[error("a CMAF rendition's timescale comes from its init segment and can't be overridden")]
158 TimescaleOverride,
159
160 #[error("sample duration {0} does not fit the 32-bit trun field")]
163 SampleDurationTooLarge(u64),
164
165 #[error("sample duration is shorter than one tick at timescale {0}")]
167 SampleDurationTooSmall(u64),
168
169 #[error("sample duration is not exactly representable at timescale {0}")]
171 SampleDurationInexact(u64),
172
173 #[error("duration-less video needs stated durations or presentation-ordered inference")]
175 MissingVideoDuration,
176}
177
178impl From<mp4_atom::Error> for Error {
179 fn from(err: mp4_atom::Error) -> Self {
180 Error::Mp4(std::sync::Arc::new(err))
181 }
182}
183
184pub type Result<T> = std::result::Result<T, Error>;
185
186pub struct Wire {
195 trak: Box<mp4_atom::Trak>,
196}
197
198impl Wire {
199 pub fn new(trak: mp4_atom::Trak) -> Self {
201 Self { trak: Box::new(trak) }
202 }
203
204 pub fn from_init(init_data: &[u8]) -> Result<Self> {
206 use mp4_atom::DecodeMaybe;
207
208 let mut cursor = std::io::Cursor::new(init_data);
209 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
210 if let mp4_atom::Any::Moov(mut moov) = atom {
211 return match moov.trak.len() {
212 1 => Ok(Self::new(moov.trak.remove(0))),
213 0 => Err(Error::NoTracks),
214 _ => Err(Error::MultipleTracks),
215 };
216 }
217 }
218 Err(Error::NoMoov)
219 }
220
221 pub fn trak(&self) -> &mp4_atom::Trak {
222 &self.trak
223 }
224}
225
226impl Container for Wire {
227 type Error = Error;
228
229 fn write(&self, group: &mut moq_net::group::Producer, frames: &[Frame]) -> std::result::Result<(), Self::Error> {
230 let timescale = moq_net::Timescale::new(self.trak.mdia.mdhd.timescale as u64)?;
231 let track_id = self.trak.tkhd.track_id;
232 encode(group, frames, timescale, track_id)
233 }
234
235 fn poll_read(
236 &self,
237 group: &mut moq_net::group::Consumer,
238 waiter: &kio::Waiter,
239 ) -> Poll<std::result::Result<Option<Vec<Frame>>, Self::Error>> {
240 use std::task::ready;
241
242 let Some(frame) = ready!(group.poll_read_frame(waiter)?) else {
243 return Poll::Ready(Ok(None));
244 };
245
246 let timescale = moq_net::Timescale::new(self.trak.mdia.mdhd.timescale as u64)?;
247 Poll::Ready(Ok(Some(decode(frame.payload, timescale)?)))
248 }
249}
250
251pub(crate) fn decode(data: Bytes, timescale: moq_net::Timescale) -> Result<Vec<Frame>> {
252 use mp4_atom::DecodeMaybe;
253
254 let mut cursor = std::io::Cursor::new(&data);
255 let mut moof = None;
256 let mut mdat_data = None;
257
258 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
259 match atom {
260 mp4_atom::Any::Moof(m) => moof = Some(m),
261 mp4_atom::Any::Mdat(m) => mdat_data = Some(m.data),
262 _ => {}
263 }
264 }
265
266 let moof = moof.ok_or(Error::NoMoof)?;
267 let mdat_data = mdat_data.ok_or(Error::NoMdat)?;
268 let traf = moof.traf.first().ok_or(Error::NoTraf)?;
269 let tfdt = traf.tfdt.as_ref().ok_or(Error::NoTfdt)?;
270 let base_dts = tfdt.base_media_decode_time;
271
272 let default_size = traf.tfhd.default_sample_size;
273 let default_duration = traf.tfhd.default_sample_duration;
274
275 let total_samples: usize = traf.trun.iter().map(|t| t.entries.len()).sum();
279
280 let mut frames = Vec::new();
281 let mut offset = 0usize;
282 let mut dts = base_dts;
283 let mut sample_index = 0usize;
284
285 for trun in &traf.trun {
286 for entry in &trun.entries {
287 let size = entry.size.or(default_size).unwrap_or(0) as usize;
288 let end = offset + size;
289
290 if end > mdat_data.len() {
291 return Err(Error::SampleRangeOutOfBounds {
292 start: offset,
293 end,
294 len: mdat_data.len(),
295 });
296 }
297
298 let cts = entry.cts.unwrap_or_default() as i64;
299 let pts = dts.checked_add_signed(cts).ok_or(Error::PtsOverflow)?;
300 let timestamp = Timestamp::new(pts, timescale)?;
302 let payload = Bytes::copy_from_slice(&mdat_data[offset..end]);
303 let flags = entry.flags.unwrap_or(0);
304 let keyframe = (flags >> 24) & 0x3 == 0x2;
306
307 let sample_duration = entry.duration.or(default_duration).filter(|d| *d != 0);
310
311 let is_last = sample_index + 1 == total_samples;
314 if sample_duration.is_none() && !is_last {
315 return Err(Error::MissingSampleDuration);
316 }
317
318 let duration = sample_duration
319 .map(|d| Timestamp::new(d as u64, timescale))
320 .transpose()?;
321
322 frames.push(Frame {
323 timestamp,
324 payload,
325 keyframe,
326 duration,
327 });
328
329 offset = end;
330 dts += sample_duration.unwrap_or(0) as u64;
331 sample_index += 1;
332 }
333 }
334
335 Ok(frames)
336}
337
338pub(crate) fn encode(
339 group: &mut moq_net::group::Producer,
340 frames: &[Frame],
341 timescale: moq_net::Timescale,
342 track_id: u32,
343) -> Result<()> {
344 if frames.is_empty() {
345 return Ok(());
346 }
347
348 let sequence_number = group.frame_count() as u32;
349 let info = FragmentInfo {
350 track_id,
351 timescale,
352 sequence_number,
353 };
354 let bytes = encode_fragment(info, frames)?;
355 let mut writer = group.create_frame(moq_net::frame::Info {
358 size: bytes.len() as u64,
359 timestamp: frames[0].timestamp,
360 })?;
361 writer.write(bytes)?;
362 writer.finish()?;
363
364 Ok(())
365}
366
367#[derive(Debug, Clone, Copy)]
372pub(crate) struct FragmentInfo {
373 pub track_id: u32,
375 pub timescale: moq_net::Timescale,
377 pub sequence_number: u32,
379}
380
381pub(crate) fn encode_fragment(info: FragmentInfo, frames: &[Frame]) -> Result<Bytes> {
389 let Some(first) = frames.first() else {
390 return Ok(Bytes::new());
391 };
392 encode_at(info, base_ticks(first, info.timescale)?, frames)
393}
394
395fn base_ticks(frame: &Frame, timescale: moq_net::Timescale) -> Result<u64> {
401 timestamp_ticks(frame.timestamp, timescale)
402}
403
404fn timestamp_ticks(timestamp: Timestamp, timescale: moq_net::Timescale) -> Result<u64> {
406 let source_scale = u128::from(timestamp.scale().as_u64());
407 let scaled = u128::from(timestamp.value()) * u128::from(timescale.as_u64());
408 let ticks = (scaled + source_scale / 2) / source_scale;
409 u64::try_from(ticks).map_err(|_| Error::PtsOverflow)
410}
411
412fn encode_at(info: FragmentInfo, base_dts: u64, frames: &[Frame]) -> Result<Bytes> {
429 let FragmentInfo {
430 track_id,
431 timescale,
432 sequence_number,
433 } = info;
434
435 use mp4_atom::Encode;
436
437 if frames.is_empty() {
438 return Ok(Bytes::new());
439 }
440
441 let mut dts = base_dts;
442
443 let entries: Vec<_> = frames
444 .iter()
445 .map(|f| {
446 let flags = if f.keyframe { 0x0200_0000 } else { 0x0001_0000 };
447 let duration = f.duration.map(|d| trun_duration(d, timescale)).transpose()?;
450 let pts = i128::from(timestamp_ticks(f.timestamp, timescale)?);
451 let cts = pts - i128::from(dts);
452 let cts = i32::try_from(cts).map_err(|_| Error::PtsOverflow)?;
453
454 if let Some(duration) = duration {
457 dts = dts.checked_add(u64::from(duration)).ok_or(Error::PtsOverflow)?;
458 }
459
460 Ok(mp4_atom::TrunEntry {
461 duration,
462 size: Some(f.payload.len() as u32),
463 flags: Some(flags),
464 cts: (cts != 0).then_some(cts),
465 })
466 })
467 .collect::<Result<_>>()?;
468
469 let mdat_data: Vec<u8> = frames.iter().flat_map(|f| f.payload.iter().copied()).collect();
470
471 let build_moof = |data_offset| mp4_atom::Moof {
472 mfhd: mp4_atom::Mfhd { sequence_number },
473 traf: vec![mp4_atom::Traf {
474 tfhd: mp4_atom::Tfhd {
475 track_id,
476 ..Default::default()
477 },
478 tfdt: Some(mp4_atom::Tfdt {
479 base_media_decode_time: base_dts,
480 }),
481 trun: vec![mp4_atom::Trun {
482 data_offset: Some(data_offset),
483 entries: entries.clone(),
484 }],
485 ..Default::default()
486 }],
487 };
488
489 let mut buf = Vec::new();
491 build_moof(0).encode(&mut buf)?;
492 let moof_size = buf.len();
493
494 buf.clear();
496 build_moof((moof_size + 8) as i32).encode(&mut buf)?;
497
498 let mdat = mp4_atom::Mdat { data: mdat_data };
499 mdat.encode(&mut buf)?;
500
501 Ok(Bytes::from(buf))
502}
503
504fn trun_duration(duration: Timestamp, timescale: moq_net::Timescale) -> Result<u32> {
506 let source_timescale = duration.scale().as_u64();
507 let output_timescale = timescale.as_u64();
508 let scaled = u128::from(duration.value()) * u128::from(output_timescale);
509 let ticks = scaled / u128::from(source_timescale);
510 if !duration.is_zero() && ticks == 0 {
511 return Err(Error::SampleDurationTooSmall(timescale.as_u64()));
512 }
513 if scaled % u128::from(source_timescale) != 0 {
514 return Err(Error::SampleDurationInexact(output_timescale));
515 }
516 u32::try_from(ticks).map_err(|_| Error::SampleDurationTooLarge(u64::try_from(ticks).unwrap_or(u64::MAX)))
517}
518
519pub(crate) fn synthesize_video_trak(
527 track_id: u32,
528 timescale: u64,
529 config: &VideoConfig,
530 description: Option<&[u8]>,
531) -> Result<mp4_atom::Trak> {
532 let width = config.coded_width.unwrap_or(0) as u16;
533 let height = config.coded_height.unwrap_or(0) as u16;
534 let visual = mp4_atom::Visual {
535 data_reference_index: 1,
536 width,
537 height,
538 ..Default::default()
539 };
540
541 let require_description = || description.ok_or_else(|| Error::MissingVideoDescription(config.codec.to_string()));
543
544 let sample_entry = match &config.codec {
545 VideoCodec::H264(_) => {
546 let mut cursor = std::io::Cursor::new(require_description()?);
547 let avcc = mp4_atom::Avcc::decode_body(&mut cursor).map_err(Error::from)?;
548 mp4_atom::Codec::from(mp4_atom::Avc1 {
549 visual,
550 avcc,
551 ..Default::default()
552 })
553 }
554 VideoCodec::H265(h265) => {
555 let mut cursor = std::io::Cursor::new(require_description()?);
556 let hvcc = mp4_atom::Hvcc::decode_body(&mut cursor).map_err(Error::from)?;
557 if h265.in_band {
559 mp4_atom::Codec::from(mp4_atom::Hev1 {
560 visual,
561 hvcc,
562 ..Default::default()
563 })
564 } else {
565 mp4_atom::Codec::from(mp4_atom::Hvc1 {
566 visual,
567 hvcc,
568 ..Default::default()
569 })
570 }
571 }
572 VideoCodec::AV1(av1) => mp4_atom::Codec::from(mp4_atom::Av01 {
573 visual,
574 av1c: crate::codec::av1::av1c_from_av1(av1),
575 ..Default::default()
576 }),
577 VideoCodec::VP8 => mp4_atom::Codec::from(mp4_atom::Vp08 {
578 visual,
579 vpcc: crate::codec::vp8::vpcc(),
580 ..Default::default()
581 }),
582 VideoCodec::VP9(vp9) => mp4_atom::Codec::from(mp4_atom::Vp09 {
583 visual,
584 vpcc: crate::codec::vp9::vpcc(vp9),
585 ..Default::default()
586 }),
587 other => return Err(Error::UnsupportedSynthesis(format!("video codec {:?}", other))),
588 };
589
590 Ok(build_video_trak(
591 track_id,
592 mdhd_timescale(timescale)?,
593 sample_entry,
594 width,
595 height,
596 ))
597}
598
599pub(crate) fn synthesize_audio_trak(track_id: u32, timescale: u64, config: &AudioConfig) -> Result<mp4_atom::Trak> {
601 use mp4_atom::Decode;
602
603 let audio = mp4_atom::Audio {
604 data_reference_index: 1,
605 channel_count: config.channel_count as u16,
606 sample_size: 16,
607 sample_rate: mp4_atom::FixedPoint::from(config.sample_rate as u16),
608 };
609
610 let sample_entry = match &config.codec {
611 AudioCodec::Opus => {
612 let pre_skip = match &config.description {
613 Some(description) => {
614 let mut description = description.as_ref();
615 crate::codec::opus::Config::parse(&mut description)?.pre_skip
616 }
617 None => 0,
618 };
619 mp4_atom::Codec::from(mp4_atom::Opus {
620 audio,
621 dops: mp4_atom::Dops {
622 output_channel_count: config.channel_count as u8,
623 pre_skip,
624 input_sample_rate: config.sample_rate,
625 output_gain: 0,
626 },
627 btrt: None,
628 })
629 }
630 AudioCodec::AAC(_) => {
631 let description = config
636 .description
637 .as_ref()
638 .ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
639 let mut cursor = std::io::Cursor::new(description.as_ref());
640 let dec_specific = mp4_atom::esds::DecoderSpecific::decode(&mut cursor)?;
641
642 let bitrate = config.bitrate.and_then(|b| u32::try_from(b).ok()).filter(|b| *b > 0);
650 let (max_bitrate, avg_bitrate) = match bitrate {
651 Some(bitrate) => (bitrate, bitrate),
652 None => (256_000, 128_000),
653 };
654 mp4_atom::Codec::from(mp4_atom::Mp4a {
655 audio,
656 esds: mp4_atom::Esds {
657 es_desc: mp4_atom::esds::EsDescriptor {
658 es_id: 0,
660 dec_config: mp4_atom::esds::DecoderConfig {
661 object_type_indication: 0x40, stream_type: 0x05, up_stream: 0,
664 buffer_size_db: mp4_atom::u24::from([0x00, 0x60, 0x00]),
666 max_bitrate,
667 avg_bitrate,
668 dec_specific: Some(dec_specific),
669 },
670 sl_config: Default::default(),
671 },
672 },
673 btrt: None,
674 taic: None,
675 })
676 }
677 AudioCodec::Flac => {
678 let description = config
681 .description
682 .as_ref()
683 .ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
684 let info = crate::codec::flac::Config::parse(&mut description.as_ref())?;
685
686 let stream_info = mp4_atom::FlacMetadataBlock::StreamInfo {
687 minimum_block_size: info.min_block_size,
688 maximum_block_size: info.max_block_size,
689 minimum_frame_size: info.min_frame_size.min(0xFF_FFFF).try_into().expect("fits in u24"),
691 maximum_frame_size: info.max_frame_size.min(0xFF_FFFF).try_into().expect("fits in u24"),
692 sample_rate: info.sample_rate,
693 num_channels_minus_one: info.channel_count.saturating_sub(1) as u8,
694 bits_per_sample_minus_one: info.bits_per_sample.saturating_sub(1) as u8,
695 number_of_interchannel_samples: info.total_samples,
696 md5_checksum: info.md5.to_vec(),
697 };
698
699 mp4_atom::Codec::from(mp4_atom::Flac {
700 audio,
701 dfla: mp4_atom::Dfla {
702 blocks: vec![stream_info],
703 },
704 })
705 }
706 other => return Err(Error::UnsupportedSynthesis(format!("audio codec {:?}", other))),
707 };
708
709 Ok(build_audio_trak(track_id, mdhd_timescale(timescale)?, sample_entry))
710}
711
712const UNKNOWN_DURATION: u64 = u64::MAX;
716
717fn build_video_trak(
718 track_id: u32,
719 timescale: u32,
720 sample_entry: mp4_atom::Codec,
721 width: u16,
722 height: u16,
723) -> mp4_atom::Trak {
724 mp4_atom::Trak {
725 tkhd: mp4_atom::Tkhd {
726 track_id,
727 enabled: true,
728 in_movie: true,
731 duration: UNKNOWN_DURATION,
732 width: mp4_atom::FixedPoint::from(width),
733 height: mp4_atom::FixedPoint::from(height),
734 ..Default::default()
735 },
736 mdia: build_mdia(timescale, b"vide", true, sample_entry),
737 ..Default::default()
738 }
739}
740
741fn build_audio_trak(track_id: u32, timescale: u32, sample_entry: mp4_atom::Codec) -> mp4_atom::Trak {
742 mp4_atom::Trak {
743 tkhd: mp4_atom::Tkhd {
744 track_id,
745 enabled: true,
746 in_movie: true,
747 duration: UNKNOWN_DURATION,
748 volume: mp4_atom::FixedPoint::from(1),
751 ..Default::default()
752 },
753 mdia: build_mdia(timescale, b"soun", false, sample_entry),
754 ..Default::default()
755 }
756}
757
758pub(crate) fn encode_init(
765 ftyp: Option<mp4_atom::Ftyp>,
766 traks: Vec<mp4_atom::Trak>,
767 trexs: Vec<mp4_atom::Trex>,
768) -> Result<Bytes> {
769 use mp4_atom::Encode;
770
771 let ftyp = ftyp.unwrap_or(mp4_atom::Ftyp {
772 major_brand: b"isom".into(),
773 minor_version: 0x200,
774 compatible_brands: vec![b"isom".into(), b"iso6".into(), b"mp41".into()],
775 });
776 let timescale = traks.first().map(|t| t.mdia.mdhd.timescale).unwrap_or(1000);
777 let next_track_id = traks.iter().map(|t| t.tkhd.track_id).max().unwrap_or(0) + 1;
778
779 let moov = mp4_atom::Moov {
780 mvhd: mp4_atom::Mvhd {
781 timescale,
782 duration: UNKNOWN_DURATION,
783 rate: mp4_atom::FixedPoint::from(1),
786 volume: mp4_atom::FixedPoint::from(1),
787 next_track_id,
789 ..Default::default()
790 },
791 trak: traks,
792 mvex: (!trexs.is_empty()).then(|| mp4_atom::Mvex {
793 trex: trexs,
794 ..Default::default()
795 }),
796 ..Default::default()
797 };
798
799 let mut buf = Vec::new();
800 ftyp.encode(&mut buf)?;
801 moov.encode(&mut buf)?;
802 Ok(Bytes::from(buf))
803}
804
805fn mdhd_timescale(timescale: u64) -> Result<u32> {
811 u32::try_from(timescale).map_err(|_| Error::TimescaleTooLarge(timescale))
812}
813
814fn build_mdia(timescale: u32, handler: &[u8; 4], is_video: bool, sample_entry: mp4_atom::Codec) -> mp4_atom::Mdia {
815 mp4_atom::Mdia {
816 mdhd: mp4_atom::Mdhd {
817 timescale,
818 ..Default::default()
819 },
820 hdlr: mp4_atom::Hdlr {
821 handler: mp4_atom::FourCC::new(handler),
822 name: String::new(),
823 },
824 minf: mp4_atom::Minf {
825 vmhd: is_video.then(mp4_atom::Vmhd::default),
826 smhd: (!is_video).then(mp4_atom::Smhd::default),
827 dinf: mp4_atom::Dinf {
828 dref: mp4_atom::Dref {
829 urls: vec![mp4_atom::Url::default()],
830 },
831 },
832 stbl: mp4_atom::Stbl {
833 stsd: mp4_atom::Stsd {
834 codecs: vec![sample_entry],
835 },
836 ..Default::default()
837 },
838 ..Default::default()
839 },
840 }
841}
842
843pub(crate) fn default_video_timescale(config: &VideoConfig) -> u64 {
854 usable_video_framerate(config)
855 .and_then(select_video_timescale)
856 .unwrap_or(90_000)
857}
858
859pub(crate) fn usable_video_framerate(config: &VideoConfig) -> Option<f64> {
861 config
862 .framerate
863 .filter(|fps| fps.is_finite() && *fps > 0.0 && (*fps * 1000.0) as u64 > 0)
864 .filter(|fps| select_video_timescale(*fps).is_some())
865}
866
867fn select_video_timescale(framerate: f64) -> Option<u64> {
869 let preferred = (framerate * 1000.0) as u64;
870
871 let frame = Duration::from_secs_f64(1.0 / framerate);
872 for timescale in [preferred, (framerate * 1001.0).round() as u64] {
873 if duration_fits_trun(frame, timescale) {
874 return Some(timescale);
875 }
876 }
877
878 const NANOS_PER_SECOND: u128 = 1_000_000_000;
879 let exact = NANOS_PER_SECOND / gcd(frame.as_nanos(), NANOS_PER_SECOND);
880 let exact = u64::try_from(exact).ok()?;
881 if duration_fits_trun(frame, exact) {
882 return Some(exact);
883 }
884 if let Some(timescale) = rational_timescale(frame) {
885 return Some(timescale);
886 }
887
888 let max_scale = u128::from(u32::MAX)
889 .checked_mul(NANOS_PER_SECOND)?
890 .checked_div(frame.as_nanos())?
891 .min(u128::from(u32::MAX));
892 let max_scale = u64::try_from(max_scale).ok()?;
893 duration_fits_trun(frame, max_scale).then_some(max_scale)
894}
895
896fn rational_timescale(duration: Duration) -> Option<u64> {
898 const NANOS_PER_SECOND: u128 = 1_000_000_000;
899 let mut numerator = duration.as_nanos();
900 let mut denominator = NANOS_PER_SECOND;
901 let (mut previous_ticks, mut ticks) = (0_u128, 1_u128);
902 let (mut previous_scale, mut scale) = (1_u128, 0_u128);
903
904 while denominator != 0 {
905 let coefficient = numerator / denominator;
906 let next_ticks = coefficient.checked_mul(ticks)?.checked_add(previous_ticks)?;
907 let next_scale = coefficient.checked_mul(scale)?.checked_add(previous_scale)?;
908 if next_ticks > u128::from(u32::MAX) || next_scale > u128::from(u32::MAX) {
909 break;
910 }
911
912 let candidate = u64::try_from(next_scale).ok()?;
913 if duration_fits_trun(duration, candidate) {
914 return Some(candidate);
915 }
916
917 (previous_ticks, ticks) = (ticks, next_ticks);
918 (previous_scale, scale) = (scale, next_scale);
919 (numerator, denominator) = (denominator, numerator % denominator);
920 }
921
922 None
923}
924
925fn duration_fits_trun(duration: Duration, timescale: u64) -> bool {
927 timescale > 0 && rounded_duration_ticks(duration, timescale).is_some_and(|ticks| u32::try_from(ticks).is_ok())
928}
929
930fn rounded_duration_ticks(duration: Duration, timescale: u64) -> Option<u64> {
932 const NANOS_PER_SECOND: u128 = 1_000_000_000;
933 let scaled = duration.as_nanos().checked_mul(u128::from(timescale))?;
934 let rounded = scaled.checked_add(NANOS_PER_SECOND / 2)? / NANOS_PER_SECOND;
935 let exact = rounded.checked_mul(NANOS_PER_SECOND)?;
936 if scaled.abs_diff(exact) > u128::from(timescale) {
937 return None;
938 }
939 u64::try_from(rounded).ok()
940}
941
942fn gcd(mut a: u128, mut b: u128) -> u128 {
944 while b != 0 {
945 (a, b) = (b, a % b);
946 }
947 a
948}
949
950#[cfg(test)]
953pub(crate) fn timeline(fragment: &Bytes) -> (u64, Vec<i32>) {
954 let traf = first_traf(fragment);
955 let cts = traf.trun[0].entries.iter().map(|e| e.cts.unwrap_or_default()).collect();
956 (traf.tfdt.as_ref().unwrap().base_media_decode_time, cts)
957}
958
959#[cfg(test)]
961pub(crate) fn sample_durations(fragment: &Bytes) -> Vec<Option<u32>> {
962 first_traf(fragment).trun[0]
963 .entries
964 .iter()
965 .map(|e| e.duration)
966 .collect()
967}
968
969#[cfg(test)]
970fn first_traf(fragment: &Bytes) -> mp4_atom::Traf {
971 use mp4_atom::DecodeMaybe;
972
973 let mut cursor = std::io::Cursor::new(fragment.as_ref());
974 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
975 if let mp4_atom::Any::Moof(moof) = atom {
976 return moof.traf.into_iter().next().expect("a traf");
977 }
978 }
979 panic!("no moof");
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985
986 fn ts(micros: u64) -> Timestamp {
987 Timestamp::from_micros(micros).unwrap()
988 }
989
990 fn info(track_id: u32, timescale: moq_net::Timescale, sequence_number: u32) -> FragmentInfo {
991 FragmentInfo {
992 track_id,
993 timescale,
994 sequence_number,
995 }
996 }
997
998 fn aac_config(bitrate: Option<u64>) -> AudioConfig {
1000 let mut config = AudioConfig::new(AudioCodec::AAC(hang::catalog::AAC { profile: 2 }), 44_100, 2);
1001 config.description = Some(Bytes::from_static(&[0x12, 0x10]));
1002 config.bitrate = bitrate;
1003 config
1004 }
1005
1006 fn moov(init: &Bytes) -> mp4_atom::Moov {
1009 use mp4_atom::DecodeMaybe;
1010
1011 let mut cursor = std::io::Cursor::new(init.as_ref());
1012 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
1013 if let mp4_atom::Any::Moov(moov) = atom {
1014 return moov;
1015 }
1016 }
1017 panic!("no moov");
1018 }
1019
1020 fn dec_config(trak: &mp4_atom::Trak) -> mp4_atom::esds::DecoderConfig {
1021 match &trak.mdia.minf.stbl.stsd.codecs[0] {
1022 mp4_atom::Codec::Mp4a(mp4a) => mp4a.esds.es_desc.dec_config.clone(),
1023 other => panic!("expected mp4a, got {other:?}"),
1024 }
1025 }
1026
1027 #[test]
1031 fn synthesized_aac_init_has_non_zero_bitrates() {
1032 let inferred = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap());
1033 assert_ne!(u32::from(inferred.buffer_size_db), 0);
1034 assert_ne!(inferred.max_bitrate, 0);
1035 assert_ne!(inferred.avg_bitrate, 0);
1036
1037 let stated = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(Some(96_000))).unwrap());
1038 assert_eq!(stated.max_bitrate, 96_000, "the catalog's bitrate wins");
1039 assert_eq!(stated.avg_bitrate, 96_000);
1040
1041 for unusable in [Some(0), Some(u64::from(u32::MAX) + 1)] {
1044 let config = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(unusable)).unwrap());
1045 assert_eq!(config.max_bitrate, inferred.max_bitrate, "{unusable:?}");
1046 assert_eq!(config.avg_bitrate, inferred.avg_bitrate, "{unusable:?}");
1047 }
1048 }
1049
1050 #[test]
1053 fn default_video_timescale_ignores_an_unusable_framerate() {
1054 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1055 for unusable in [0.0, -30.0, f64::NAN, f64::INFINITY, 0.0005] {
1056 config.framerate = Some(unusable);
1057 assert_eq!(default_video_timescale(&config), 90_000, "{unusable}");
1058 }
1059
1060 config.framerate = Some(30.0);
1061 assert_eq!(default_video_timescale(&config), 30_000);
1062
1063 config.framerate = Some(30_000.0 / 1001.0);
1064 assert_eq!(default_video_timescale(&config), 30_000);
1065 }
1066
1067 #[test]
1068 fn default_video_timescale_keeps_low_cadence_within_trun() {
1069 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1070 for framerate in [0.2001, 0.0011] {
1071 config.framerate = Some(framerate);
1072 let timescale = default_video_timescale(&config);
1073 let duration = Duration::from_secs_f64(1.0 / framerate);
1074 let ticks = rounded_duration_ticks(duration, timescale).unwrap();
1075
1076 assert!(timescale <= u64::from(u32::MAX));
1077 assert!(ticks <= u64::from(u32::MAX));
1078 }
1079
1080 assert_eq!(default_video_timescale(&config), 11);
1081 }
1082
1083 #[test]
1086 fn synthesized_init_declares_unknown_duration() {
1087 let trak = synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap();
1088 assert_eq!(trak.tkhd.duration, u64::MAX);
1089
1090 let init = encode_init(None, vec![trak], Vec::new()).unwrap();
1091 let moov = moov(&init);
1092 assert_eq!(moov.mvhd.duration, u64::MAX);
1093 assert_eq!(moov.trak[0].tkhd.duration, u64::MAX);
1094 }
1095
1096 #[test]
1100 fn synthesized_init_headers_describe_a_playable_presentation() {
1101 let audio = synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap();
1102 let init = encode_init(None, vec![audio], Vec::new()).unwrap();
1103 let moov = moov(&init);
1104
1105 assert_eq!(moov.mvhd.rate.integer(), 1, "normal playback rate");
1106 assert_eq!(moov.mvhd.volume.integer(), 1, "full volume");
1107 assert_eq!(moov.mvhd.next_track_id, 2, "past the only track id");
1108
1109 let tkhd = &moov.trak[0].tkhd;
1110 assert!(tkhd.enabled && tkhd.in_movie, "flags 0x000003");
1111 assert_eq!(tkhd.volume.integer(), 1, "an audio track carries the volume");
1112 }
1113
1114 #[test]
1116 fn synthesized_video_init_sets_the_track_flags() {
1117 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
1118 config.framerate = Some(30.0);
1119 let video = synthesize_video_trak(1, 30_000, &config, None).unwrap();
1120 let init = encode_init(None, vec![video], Vec::new()).unwrap();
1121 let moov = moov(&init);
1122
1123 let tkhd = &moov.trak[0].tkhd;
1124 assert!(tkhd.enabled && tkhd.in_movie, "flags 0x000003");
1125 assert_eq!(tkhd.volume.integer(), 0);
1126 }
1127
1128 #[test]
1129 fn decode_reads_trun_sample_duration() {
1130 use mp4_atom::Encode;
1131
1132 let timescale = moq_net::Timescale::MICRO;
1136 let moof = mp4_atom::Moof {
1137 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
1138 traf: vec![mp4_atom::Traf {
1139 tfhd: mp4_atom::Tfhd {
1140 track_id: 1,
1141 ..Default::default()
1142 },
1143 tfdt: Some(mp4_atom::Tfdt {
1144 base_media_decode_time: 0,
1145 }),
1146 trun: vec![mp4_atom::Trun {
1147 data_offset: Some(0),
1148 entries: vec![
1149 mp4_atom::TrunEntry {
1150 size: Some(2),
1151 duration: Some(33_333),
1152 ..Default::default()
1153 },
1154 mp4_atom::TrunEntry {
1155 size: Some(2),
1156 duration: Some(33_333),
1157 ..Default::default()
1158 },
1159 ],
1160 }],
1161 ..Default::default()
1162 }],
1163 };
1164
1165 let mut buf = Vec::new();
1166 moof.encode(&mut buf).unwrap();
1167 mp4_atom::Mdat {
1168 data: vec![0xDE, 0xAD, 0xBE, 0xEF],
1169 }
1170 .encode(&mut buf)
1171 .unwrap();
1172
1173 let frames = decode(Bytes::from(buf), timescale).unwrap();
1174 assert_eq!(frames.len(), 2);
1175 assert_eq!(frames[0].timestamp, ts(0));
1176 assert_eq!(frames[0].duration, Some(ts(33_333)));
1177 assert_eq!(frames[1].timestamp, ts(33_333));
1178 assert_eq!(frames[1].duration, Some(ts(33_333)));
1179 }
1180
1181 #[test]
1182 fn duration_round_trips_through_encode() {
1183 let timescale = moq_net::Timescale::MICRO;
1185 let input = vec![Frame {
1186 timestamp: ts(0),
1187 payload: Bytes::from_static(&[0xDE, 0xAD]),
1188 keyframe: true,
1189 duration: Some(ts(33_333)),
1190 }];
1191
1192 let fragment = encode_fragment(info(1, timescale, 0), &input).unwrap();
1193 let frames = decode(fragment, timescale).unwrap();
1194
1195 assert_eq!(frames.len(), 1);
1196 assert_eq!(frames[0].duration, Some(ts(33_333)));
1197 }
1198
1199 #[test]
1202 fn encode_fragment_rejects_a_duration_too_large_for_trun() {
1203 let timescale = moq_net::Timescale::new(u64::from(u32::MAX)).unwrap();
1204 let over = u64::from(u32::MAX) + 1;
1205 let frame = Frame {
1206 timestamp: Timestamp::from_scale(0, timescale.as_u64()).unwrap(),
1207 payload: Bytes::from_static(&[0xDE, 0xAD]),
1208 keyframe: true,
1209 duration: Some(Timestamp::from_scale(over, timescale.as_u64()).unwrap()),
1210 };
1211
1212 let err = encode_fragment(info(1, timescale, 0), std::slice::from_ref(&frame)).unwrap_err();
1213 assert!(matches!(err, Error::SampleDurationTooLarge(ticks) if ticks == over));
1214
1215 let largest = Frame {
1216 duration: Some(Timestamp::from_scale(u64::from(u32::MAX), timescale.as_u64()).unwrap()),
1217 ..frame
1218 };
1219 let fragment = encode_fragment(info(1, timescale, 0), &[largest]).unwrap();
1220 assert_eq!(sample_durations(&fragment), vec![Some(u32::MAX)]);
1221 }
1222
1223 #[test]
1226 fn encode_fragment_rejects_a_duration_shorter_than_one_tick() {
1227 let timescale = moq_net::Timescale::SECOND;
1228 let frame = Frame {
1229 timestamp: Timestamp::from_secs(0).unwrap(),
1230 payload: Bytes::from_static(&[0xDE, 0xAD]),
1231 keyframe: true,
1232 duration: Some(Timestamp::from_millis(33).unwrap()),
1233 };
1234
1235 let err = encode_fragment(info(1, timescale, 0), std::slice::from_ref(&frame)).unwrap_err();
1236 assert!(matches!(err, Error::SampleDurationTooSmall(1)));
1237
1238 let one_tick = Frame {
1239 duration: Some(Timestamp::from_secs(1).unwrap()),
1240 ..frame
1241 };
1242 let fragment = encode_fragment(info(1, timescale, 0), &[one_tick]).unwrap();
1243 assert_eq!(sample_durations(&fragment), vec![Some(1)]);
1244 }
1245
1246 #[test]
1249 fn encode_fragment_rejects_an_inexact_sample_duration() {
1250 let input_scale = moq_net::Timescale::new(24).unwrap();
1251 let output_scale = moq_net::Timescale::MILLI;
1252 let frame = Frame {
1253 timestamp: Timestamp::new(0, input_scale).unwrap(),
1254 payload: Bytes::from_static(&[0xDE, 0xAD]),
1255 keyframe: true,
1256 duration: Some(Timestamp::new(1, input_scale).unwrap()),
1257 };
1258
1259 let err = encode_fragment(info(1, output_scale, 0), std::slice::from_ref(&frame)).unwrap_err();
1260 assert!(matches!(err, Error::SampleDurationInexact(1_000)));
1261
1262 let exact_scale = moq_net::Timescale::new(24_000).unwrap();
1263 let fragment = encode_fragment(info(1, exact_scale, 0), &[frame]).unwrap();
1264 assert_eq!(sample_durations(&fragment), vec![Some(1_000)]);
1265 }
1266
1267 #[test]
1270 fn encode_fragment_rejects_a_pts_too_large_for_tfdt() {
1271 let timescale = moq_net::Timescale::new(u64::from(u32::MAX)).unwrap();
1272 let frame = Frame {
1273 timestamp: Timestamp::from_secs(1 << 40).unwrap(),
1274 payload: Bytes::from_static(&[0xDE, 0xAD]),
1275 keyframe: true,
1276 duration: None,
1277 };
1278
1279 let err = encode_fragment(info(1, timescale, 0), &[frame]).unwrap_err();
1280 assert!(matches!(err, Error::PtsOverflow));
1281 }
1282
1283 #[test]
1284 fn reordered_pts_round_trips_with_cts() {
1285 let timescale = moq_net::Timescale::new(1_000_000).unwrap();
1286 let input = vec![
1287 Frame {
1288 timestamp: ts(0),
1289 payload: Bytes::from_static(&[0x00]),
1290 keyframe: true,
1291 duration: Some(ts(33_000)),
1292 },
1293 Frame {
1294 timestamp: ts(99_000),
1295 payload: Bytes::from_static(&[0x01]),
1296 keyframe: false,
1297 duration: Some(ts(33_000)),
1298 },
1299 Frame {
1300 timestamp: ts(33_000),
1301 payload: Bytes::from_static(&[0x02]),
1302 keyframe: false,
1303 duration: Some(ts(33_000)),
1304 },
1305 ];
1306
1307 let fragment = encode_fragment(info(1, timescale, 0), &input).unwrap();
1308 let frames = decode(fragment, timescale).unwrap();
1309
1310 assert_eq!(frames.len(), input.len());
1311 for (actual, expected) in frames.iter().zip(&input) {
1312 assert_eq!(actual.timestamp, expected.timestamp);
1313 assert_eq!(actual.duration, expected.duration);
1314 assert_eq!(actual.payload, expected.payload);
1315 }
1316 }
1317
1318 #[test]
1319 fn decode_without_duration_reports_none() {
1320 let timescale = moq_net::Timescale::new(90_000).unwrap();
1323 let frames = vec![Frame {
1324 timestamp: ts(0),
1325 payload: Bytes::from_static(&[0xDE, 0xAD]),
1326 keyframe: true,
1327 duration: None,
1328 }];
1329
1330 let fragment = encode_fragment(info(1, timescale, 0), &frames).unwrap();
1331 let frames = decode(fragment, timescale).unwrap();
1332
1333 assert_eq!(frames.len(), 1);
1334 assert_eq!(frames[0].duration, None);
1335 }
1336
1337 #[test]
1338 fn decode_zero_duration_reports_none() {
1339 use mp4_atom::Encode;
1340
1341 let timescale = moq_net::Timescale::new(24_000).unwrap();
1342 let moof = mp4_atom::Moof {
1343 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
1344 traf: vec![mp4_atom::Traf {
1345 tfhd: mp4_atom::Tfhd {
1346 track_id: 1,
1347 default_sample_duration: Some(0),
1348 default_sample_size: Some(2),
1349 ..Default::default()
1350 },
1351 tfdt: Some(mp4_atom::Tfdt {
1352 base_media_decode_time: 2_000,
1353 }),
1354 trun: vec![mp4_atom::Trun {
1355 data_offset: Some(0),
1356 entries: vec![mp4_atom::TrunEntry {
1357 size: None,
1358 duration: None,
1359 ..Default::default()
1360 }],
1361 }],
1362 ..Default::default()
1363 }],
1364 };
1365
1366 let mut buf = Vec::new();
1367 moof.encode(&mut buf).unwrap();
1368 mp4_atom::Mdat { data: vec![0xDE, 0xAD] }.encode(&mut buf).unwrap();
1369
1370 let frames = decode(Bytes::from(buf), timescale).unwrap();
1371 assert_eq!(frames.len(), 1);
1372 assert_eq!(frames[0].timestamp.as_micros(), 83_333);
1373 assert_eq!(frames[0].duration, None);
1374 }
1375}