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