1mod export;
10mod import;
11mod muxer;
12
13pub use export::*;
14pub use import::*;
15pub use muxer::*;
16
17#[cfg(test)]
18mod export_test;
19#[cfg(test)]
20mod import_test;
21
22use std::task::Poll;
23
24use bytes::Bytes;
25use hang::catalog::{AudioCodec, AudioConfig, VideoCodec, VideoConfig};
26use mp4_atom::Atom;
27
28use moq_net::Timestamp;
29
30use crate::container::{Container, Frame};
31
32#[derive(Debug, Clone, thiserror::Error)]
33#[non_exhaustive]
34pub enum Error {
35 #[error("mp4: {0}")]
36 Mp4(std::sync::Arc<mp4_atom::Error>),
37
38 #[error("moq: {0}")]
39 Moq(#[from] moq_net::Error),
40
41 #[error("flac: {0}")]
42 Flac(#[from] crate::codec::flac::Error),
43
44 #[error("opus: {0}")]
45 Opus(#[from] crate::codec::opus::Error),
46
47 #[error("missing keyframe: a group must open on a keyframe")]
48 MissingKeyframe(#[from] crate::container::MissingKeyframe),
49
50 #[error("timestamp overflow")]
51 TimestampOverflow(#[from] moq_net::TimeOverflow),
52
53 #[error("no traf in moof")]
54 NoTraf,
55
56 #[error("no tfdt in traf")]
57 NoTfdt,
58
59 #[error("PTS overflow")]
60 PtsOverflow,
61
62 #[error("missing moof")]
63 NoMoof,
64
65 #[error("missing mdat")]
66 NoMdat,
67
68 #[error("missing moov")]
69 NoMoov,
70
71 #[error("no tracks in moov")]
72 NoTracks,
73
74 #[error("multiple tracks in moov, use Trak instead")]
75 MultipleTracks,
76
77 #[error("can't synthesize CMAF init for {0}")]
78 UnsupportedSynthesis(String),
79
80 #[error("subtitle tracks are not supported")]
81 UnsupportedSubtitle,
82
83 #[error("unknown track handler: {0:?}")]
84 UnknownTrackHandler([u8; 4]),
85
86 #[error("missing codec")]
87 MissingCodec,
88
89 #[error("multiple codecs")]
90 MultipleCodecs,
91
92 #[error("unknown codec: {0:?}")]
93 UnknownCodec(mp4_atom::FourCC),
94
95 #[error("unsupported codec: {0:?}")]
96 UnsupportedCodec(Box<mp4_atom::Codec>),
97
98 #[error("unsupported codec: MPEG2")]
99 UnsupportedMpeg2,
100
101 #[error("duplicate moof")]
102 DuplicateMoof,
103
104 #[error("missing trun")]
105 MissingTrun,
106
107 #[error("missing tfdt")]
108 MissingTfdt,
109
110 #[error("video codec {0} needs a description (codec config record) to synthesize a CMAF init")]
111 MissingVideoDescription(String),
112
113 #[error("video track {0} missing in catalog")]
114 MissingVideoTrack(String),
115
116 #[error("audio track {0} missing in catalog")]
117 MissingAudioTrack(String),
118
119 #[error("invalid data offset")]
120 InvalidDataOffset,
121
122 #[error("unknown track {0}")]
123 UnknownTrack(u32),
124
125 #[error("no keyframe at start of group")]
126 NoKeyframe,
127
128 #[error("track sample range {start}..{end} is out of bounds of mdat (len {len})")]
129 SampleRangeOutOfBounds { start: usize, end: usize, len: usize },
130
131 #[error("no catalog snapshot")]
132 NoCatalogSnapshot,
133
134 #[error("encode_fragment called with no frames")]
135 NoFrames,
136
137 #[error("audio codec {0} needs a description (AudioSpecificConfig) to synthesize a CMAF init")]
138 MissingAudioDescription(String),
139
140 #[error("multi-sample fragment has a non-final sample with no duration; DTS is unrecoverable")]
141 MissingSampleDuration,
142
143 #[error("timescale {0} does not fit the 32-bit mdhd field")]
146 TimescaleTooLarge(u64),
147}
148
149impl From<mp4_atom::Error> for Error {
150 fn from(err: mp4_atom::Error) -> Self {
151 Error::Mp4(std::sync::Arc::new(err))
152 }
153}
154
155pub type Result<T> = std::result::Result<T, Error>;
156
157pub struct Wire {
166 trak: Box<mp4_atom::Trak>,
167}
168
169impl Wire {
170 pub fn new(trak: mp4_atom::Trak) -> Self {
172 Self { trak: Box::new(trak) }
173 }
174
175 pub fn from_init(init_data: &[u8]) -> Result<Self> {
177 use mp4_atom::DecodeMaybe;
178
179 let mut cursor = std::io::Cursor::new(init_data);
180 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
181 if let mp4_atom::Any::Moov(mut moov) = atom {
182 return match moov.trak.len() {
183 1 => Ok(Self::new(moov.trak.remove(0))),
184 0 => Err(Error::NoTracks),
185 _ => Err(Error::MultipleTracks),
186 };
187 }
188 }
189 Err(Error::NoMoov)
190 }
191
192 pub fn trak(&self) -> &mp4_atom::Trak {
193 &self.trak
194 }
195}
196
197impl Container for Wire {
198 type Error = Error;
199
200 fn write(&self, group: &mut moq_net::group::Producer, frames: &[Frame]) -> std::result::Result<(), Self::Error> {
201 let timescale = moq_net::Timescale::new(self.trak.mdia.mdhd.timescale as u64)?;
202 let track_id = self.trak.tkhd.track_id;
203 encode(group, frames, timescale, track_id)
204 }
205
206 fn poll_read(
207 &self,
208 group: &mut moq_net::group::Consumer,
209 waiter: &kio::Waiter,
210 ) -> Poll<std::result::Result<Option<Vec<Frame>>, Self::Error>> {
211 use std::task::ready;
212
213 let Some(frame) = ready!(group.poll_read_frame(waiter)?) else {
214 return Poll::Ready(Ok(None));
215 };
216
217 let timescale = moq_net::Timescale::new(self.trak.mdia.mdhd.timescale as u64)?;
218 Poll::Ready(Ok(Some(decode(frame.payload, timescale)?)))
219 }
220}
221
222pub(crate) fn decode(data: Bytes, timescale: moq_net::Timescale) -> Result<Vec<Frame>> {
223 use mp4_atom::DecodeMaybe;
224
225 let mut cursor = std::io::Cursor::new(&data);
226 let mut moof = None;
227 let mut mdat_data = None;
228
229 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
230 match atom {
231 mp4_atom::Any::Moof(m) => moof = Some(m),
232 mp4_atom::Any::Mdat(m) => mdat_data = Some(m.data),
233 _ => {}
234 }
235 }
236
237 let moof = moof.ok_or(Error::NoMoof)?;
238 let mdat_data = mdat_data.ok_or(Error::NoMdat)?;
239 let traf = moof.traf.first().ok_or(Error::NoTraf)?;
240 let tfdt = traf.tfdt.as_ref().ok_or(Error::NoTfdt)?;
241 let base_dts = tfdt.base_media_decode_time;
242
243 let default_size = traf.tfhd.default_sample_size;
244 let default_duration = traf.tfhd.default_sample_duration;
245
246 let total_samples: usize = traf.trun.iter().map(|t| t.entries.len()).sum();
250
251 let mut frames = Vec::new();
252 let mut offset = 0usize;
253 let mut dts = base_dts;
254 let mut sample_index = 0usize;
255
256 for trun in &traf.trun {
257 for entry in &trun.entries {
258 let size = entry.size.or(default_size).unwrap_or(0) as usize;
259 let end = offset + size;
260
261 if end > mdat_data.len() {
262 return Err(Error::SampleRangeOutOfBounds {
263 start: offset,
264 end,
265 len: mdat_data.len(),
266 });
267 }
268
269 let cts = entry.cts.unwrap_or_default() as i64;
270 let pts = dts.checked_add_signed(cts).ok_or(Error::PtsOverflow)?;
271 let timestamp = Timestamp::new(pts, timescale)?;
273 let payload = Bytes::copy_from_slice(&mdat_data[offset..end]);
274 let flags = entry.flags.unwrap_or(0);
275 let keyframe = (flags >> 24) & 0x3 == 0x2;
277
278 let sample_duration = entry.duration.or(default_duration).filter(|d| *d != 0);
281
282 let is_last = sample_index + 1 == total_samples;
285 if sample_duration.is_none() && !is_last {
286 return Err(Error::MissingSampleDuration);
287 }
288
289 let duration = sample_duration
290 .map(|d| Timestamp::new(d as u64, timescale))
291 .transpose()?;
292
293 frames.push(Frame {
294 timestamp,
295 payload,
296 keyframe,
297 duration,
298 });
299
300 offset = end;
301 dts += sample_duration.unwrap_or(0) as u64;
302 sample_index += 1;
303 }
304 }
305
306 Ok(frames)
307}
308
309pub(crate) fn encode(
310 group: &mut moq_net::group::Producer,
311 frames: &[Frame],
312 timescale: moq_net::Timescale,
313 track_id: u32,
314) -> Result<()> {
315 if frames.is_empty() {
316 return Ok(());
317 }
318
319 let sequence_number = group.frame_count() as u32;
320 let bytes = encode_fragment(track_id, timescale, sequence_number, frames)?;
321 let mut writer = group.create_frame(moq_net::frame::Info {
324 size: bytes.len() as u64,
325 timestamp: frames[0].timestamp,
326 })?;
327 writer.write(bytes)?;
328 writer.finish()?;
329
330 Ok(())
331}
332
333pub(crate) fn encode_fragment(
342 track_id: u32,
343 timescale: moq_net::Timescale,
344 sequence_number: u32,
345 frames: &[Frame],
346) -> Result<Bytes> {
347 use mp4_atom::Encode;
348
349 if frames.is_empty() {
350 return Ok(Bytes::new());
351 }
352
353 let base_dts = frames[0].timestamp.as_scale(timescale) as u64;
358 let mut dts = base_dts;
359
360 let entries: Vec<_> = frames
361 .iter()
362 .map(|f| {
363 let flags = if f.keyframe { 0x0200_0000 } else { 0x0001_0000 };
364 let duration = f.duration.map(|d| d.as_scale(timescale) as u32);
367 let pts = f.timestamp.as_scale(timescale) as i128;
368 let cts = pts - i128::from(dts);
369 let cts = i32::try_from(cts).map_err(|_| Error::PtsOverflow)?;
370
371 if let Some(duration) = duration {
374 dts = dts.checked_add(u64::from(duration)).ok_or(Error::PtsOverflow)?;
375 }
376
377 Ok(mp4_atom::TrunEntry {
378 duration,
379 size: Some(f.payload.len() as u32),
380 flags: Some(flags),
381 cts: (cts != 0).then_some(cts),
382 })
383 })
384 .collect::<Result<_>>()?;
385
386 let mdat_data: Vec<u8> = frames.iter().flat_map(|f| f.payload.iter().copied()).collect();
387
388 let build_moof = |data_offset| mp4_atom::Moof {
389 mfhd: mp4_atom::Mfhd { sequence_number },
390 traf: vec![mp4_atom::Traf {
391 tfhd: mp4_atom::Tfhd {
392 track_id,
393 ..Default::default()
394 },
395 tfdt: Some(mp4_atom::Tfdt {
396 base_media_decode_time: base_dts,
397 }),
398 trun: vec![mp4_atom::Trun {
399 data_offset: Some(data_offset),
400 entries: entries.clone(),
401 }],
402 ..Default::default()
403 }],
404 };
405
406 let mut buf = Vec::new();
408 build_moof(0).encode(&mut buf)?;
409 let moof_size = buf.len();
410
411 buf.clear();
413 build_moof((moof_size + 8) as i32).encode(&mut buf)?;
414
415 let mdat = mp4_atom::Mdat { data: mdat_data };
416 mdat.encode(&mut buf)?;
417
418 Ok(Bytes::from(buf))
419}
420
421pub(crate) fn synthesize_video_trak(
429 track_id: u32,
430 timescale: u64,
431 config: &VideoConfig,
432 description: Option<&[u8]>,
433) -> Result<mp4_atom::Trak> {
434 let width = config.coded_width.unwrap_or(0) as u16;
435 let height = config.coded_height.unwrap_or(0) as u16;
436 let visual = mp4_atom::Visual {
437 data_reference_index: 1,
438 width,
439 height,
440 ..Default::default()
441 };
442
443 let require_description = || description.ok_or_else(|| Error::MissingVideoDescription(config.codec.to_string()));
445
446 let sample_entry = match &config.codec {
447 VideoCodec::H264(_) => {
448 let mut cursor = std::io::Cursor::new(require_description()?);
449 let avcc = mp4_atom::Avcc::decode_body(&mut cursor).map_err(Error::from)?;
450 mp4_atom::Codec::from(mp4_atom::Avc1 {
451 visual,
452 avcc,
453 ..Default::default()
454 })
455 }
456 VideoCodec::H265(h265) => {
457 let mut cursor = std::io::Cursor::new(require_description()?);
458 let hvcc = mp4_atom::Hvcc::decode_body(&mut cursor).map_err(Error::from)?;
459 if h265.in_band {
461 mp4_atom::Codec::from(mp4_atom::Hev1 {
462 visual,
463 hvcc,
464 ..Default::default()
465 })
466 } else {
467 mp4_atom::Codec::from(mp4_atom::Hvc1 {
468 visual,
469 hvcc,
470 ..Default::default()
471 })
472 }
473 }
474 VideoCodec::AV1(av1) => mp4_atom::Codec::from(mp4_atom::Av01 {
475 visual,
476 av1c: crate::codec::av1::av1c_from_av1(av1),
477 ..Default::default()
478 }),
479 VideoCodec::VP8 => mp4_atom::Codec::from(mp4_atom::Vp08 {
480 visual,
481 vpcc: crate::codec::vp8::vpcc(),
482 ..Default::default()
483 }),
484 VideoCodec::VP9(vp9) => mp4_atom::Codec::from(mp4_atom::Vp09 {
485 visual,
486 vpcc: crate::codec::vp9::vpcc(vp9),
487 ..Default::default()
488 }),
489 other => return Err(Error::UnsupportedSynthesis(format!("video codec {:?}", other))),
490 };
491
492 Ok(build_video_trak(
493 track_id,
494 mdhd_timescale(timescale)?,
495 sample_entry,
496 width,
497 height,
498 ))
499}
500
501pub(crate) fn synthesize_audio_trak(track_id: u32, timescale: u64, config: &AudioConfig) -> Result<mp4_atom::Trak> {
503 use mp4_atom::Decode;
504
505 let audio = mp4_atom::Audio {
506 data_reference_index: 1,
507 channel_count: config.channel_count as u16,
508 sample_size: 16,
509 sample_rate: mp4_atom::FixedPoint::from(config.sample_rate as u16),
510 };
511
512 let sample_entry = match &config.codec {
513 AudioCodec::Opus => {
514 let pre_skip = match &config.description {
515 Some(description) => {
516 let mut description = description.as_ref();
517 crate::codec::opus::Config::parse(&mut description)?.pre_skip
518 }
519 None => 0,
520 };
521 mp4_atom::Codec::from(mp4_atom::Opus {
522 audio,
523 dops: mp4_atom::Dops {
524 output_channel_count: config.channel_count as u8,
525 pre_skip,
526 input_sample_rate: config.sample_rate,
527 output_gain: 0,
528 },
529 btrt: None,
530 })
531 }
532 AudioCodec::AAC(_) => {
533 let description = config
538 .description
539 .as_ref()
540 .ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
541 let mut cursor = std::io::Cursor::new(description.as_ref());
542 let dec_specific = mp4_atom::esds::DecoderSpecific::decode(&mut cursor)?;
543
544 let bitrate = config.bitrate.and_then(|b| u32::try_from(b).ok()).filter(|b| *b > 0);
552 let (max_bitrate, avg_bitrate) = match bitrate {
553 Some(bitrate) => (bitrate, bitrate),
554 None => (256_000, 128_000),
555 };
556 mp4_atom::Codec::from(mp4_atom::Mp4a {
557 audio,
558 esds: mp4_atom::Esds {
559 es_desc: mp4_atom::esds::EsDescriptor {
560 es_id: 0,
562 dec_config: mp4_atom::esds::DecoderConfig {
563 object_type_indication: 0x40, stream_type: 0x05, up_stream: 0,
566 buffer_size_db: mp4_atom::u24::from([0x00, 0x60, 0x00]),
568 max_bitrate,
569 avg_bitrate,
570 dec_specific,
571 },
572 sl_config: Default::default(),
573 },
574 },
575 btrt: None,
576 taic: None,
577 })
578 }
579 AudioCodec::Flac => {
580 let description = config
583 .description
584 .as_ref()
585 .ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
586 let info = crate::codec::flac::Config::parse(&mut description.as_ref())?;
587
588 let stream_info = mp4_atom::FlacMetadataBlock::StreamInfo {
589 minimum_block_size: info.min_block_size,
590 maximum_block_size: info.max_block_size,
591 minimum_frame_size: info.min_frame_size.min(0xFF_FFFF).try_into().expect("fits in u24"),
593 maximum_frame_size: info.max_frame_size.min(0xFF_FFFF).try_into().expect("fits in u24"),
594 sample_rate: info.sample_rate,
595 num_channels_minus_one: info.channel_count.saturating_sub(1) as u8,
596 bits_per_sample_minus_one: info.bits_per_sample.saturating_sub(1) as u8,
597 number_of_interchannel_samples: info.total_samples,
598 md5_checksum: info.md5.to_vec(),
599 };
600
601 mp4_atom::Codec::from(mp4_atom::Flac {
602 audio,
603 dfla: mp4_atom::Dfla {
604 blocks: vec![stream_info],
605 },
606 })
607 }
608 other => return Err(Error::UnsupportedSynthesis(format!("audio codec {:?}", other))),
609 };
610
611 Ok(build_audio_trak(track_id, mdhd_timescale(timescale)?, sample_entry))
612}
613
614const UNKNOWN_DURATION: u64 = u64::MAX;
618
619fn build_video_trak(
620 track_id: u32,
621 timescale: u32,
622 sample_entry: mp4_atom::Codec,
623 width: u16,
624 height: u16,
625) -> mp4_atom::Trak {
626 mp4_atom::Trak {
627 tkhd: mp4_atom::Tkhd {
628 track_id,
629 enabled: true,
630 in_movie: true,
633 duration: UNKNOWN_DURATION,
634 width: mp4_atom::FixedPoint::from(width),
635 height: mp4_atom::FixedPoint::from(height),
636 ..Default::default()
637 },
638 mdia: build_mdia(timescale, b"vide", true, sample_entry),
639 ..Default::default()
640 }
641}
642
643fn build_audio_trak(track_id: u32, timescale: u32, sample_entry: mp4_atom::Codec) -> mp4_atom::Trak {
644 mp4_atom::Trak {
645 tkhd: mp4_atom::Tkhd {
646 track_id,
647 enabled: true,
648 in_movie: true,
649 duration: UNKNOWN_DURATION,
650 volume: mp4_atom::FixedPoint::from(1),
653 ..Default::default()
654 },
655 mdia: build_mdia(timescale, b"soun", false, sample_entry),
656 ..Default::default()
657 }
658}
659
660pub(crate) fn encode_init(
667 ftyp: Option<mp4_atom::Ftyp>,
668 traks: Vec<mp4_atom::Trak>,
669 trexs: Vec<mp4_atom::Trex>,
670) -> Result<Bytes> {
671 use mp4_atom::Encode;
672
673 let ftyp = ftyp.unwrap_or(mp4_atom::Ftyp {
674 major_brand: b"isom".into(),
675 minor_version: 0x200,
676 compatible_brands: vec![b"isom".into(), b"iso6".into(), b"mp41".into()],
677 });
678 let timescale = traks.first().map(|t| t.mdia.mdhd.timescale).unwrap_or(1000);
679 let next_track_id = traks.iter().map(|t| t.tkhd.track_id).max().unwrap_or(0) + 1;
680
681 let moov = mp4_atom::Moov {
682 mvhd: mp4_atom::Mvhd {
683 timescale,
684 duration: UNKNOWN_DURATION,
685 rate: mp4_atom::FixedPoint::from(1),
688 volume: mp4_atom::FixedPoint::from(1),
689 next_track_id,
691 ..Default::default()
692 },
693 trak: traks,
694 mvex: (!trexs.is_empty()).then(|| mp4_atom::Mvex {
695 trex: trexs,
696 ..Default::default()
697 }),
698 ..Default::default()
699 };
700
701 let mut buf = Vec::new();
702 ftyp.encode(&mut buf)?;
703 moov.encode(&mut buf)?;
704 Ok(Bytes::from(buf))
705}
706
707fn mdhd_timescale(timescale: u64) -> Result<u32> {
713 u32::try_from(timescale).map_err(|_| Error::TimescaleTooLarge(timescale))
714}
715
716fn build_mdia(timescale: u32, handler: &[u8; 4], is_video: bool, sample_entry: mp4_atom::Codec) -> mp4_atom::Mdia {
717 mp4_atom::Mdia {
718 mdhd: mp4_atom::Mdhd {
719 timescale,
720 ..Default::default()
721 },
722 hdlr: mp4_atom::Hdlr {
723 handler: mp4_atom::FourCC::new(handler),
724 name: String::new(),
725 },
726 minf: mp4_atom::Minf {
727 vmhd: is_video.then(mp4_atom::Vmhd::default),
728 smhd: (!is_video).then(mp4_atom::Smhd::default),
729 dinf: mp4_atom::Dinf {
730 dref: mp4_atom::Dref {
731 urls: vec![mp4_atom::Url::default()],
732 },
733 },
734 stbl: mp4_atom::Stbl {
735 stsd: mp4_atom::Stsd {
736 codecs: vec![sample_entry],
737 },
738 ..Default::default()
739 },
740 ..Default::default()
741 },
742 }
743}
744
745pub(crate) fn default_video_timescale(config: &VideoConfig) -> u64 {
758 let ticks = config
759 .framerate
760 .filter(|fps| fps.is_finite())
761 .map(|fps| (fps * 1000.0) as u64);
762 match ticks {
763 Some(ticks) if ticks > 0 => ticks,
764 _ => 90000,
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771
772 fn ts(micros: u64) -> Timestamp {
773 Timestamp::from_micros(micros).unwrap()
774 }
775
776 fn aac_config(bitrate: Option<u64>) -> AudioConfig {
778 let mut config = AudioConfig::new(AudioCodec::AAC(hang::catalog::AAC { profile: 2 }), 44_100, 2);
779 config.description = Some(Bytes::from_static(&[0x12, 0x10]));
780 config.bitrate = bitrate;
781 config
782 }
783
784 fn moov(init: &Bytes) -> mp4_atom::Moov {
787 use mp4_atom::DecodeMaybe;
788
789 let mut cursor = std::io::Cursor::new(init.as_ref());
790 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
791 if let mp4_atom::Any::Moov(moov) = atom {
792 return moov;
793 }
794 }
795 panic!("no moov");
796 }
797
798 fn dec_config(trak: &mp4_atom::Trak) -> mp4_atom::esds::DecoderConfig {
799 match &trak.mdia.minf.stbl.stsd.codecs[0] {
800 mp4_atom::Codec::Mp4a(mp4a) => mp4a.esds.es_desc.dec_config,
801 other => panic!("expected mp4a, got {other:?}"),
802 }
803 }
804
805 #[test]
809 fn synthesized_aac_init_has_non_zero_bitrates() {
810 let inferred = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap());
811 assert_ne!(u32::from(inferred.buffer_size_db), 0);
812 assert_ne!(inferred.max_bitrate, 0);
813 assert_ne!(inferred.avg_bitrate, 0);
814
815 let stated = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(Some(96_000))).unwrap());
816 assert_eq!(stated.max_bitrate, 96_000, "the catalog's bitrate wins");
817 assert_eq!(stated.avg_bitrate, 96_000);
818
819 for unusable in [Some(0), Some(u64::from(u32::MAX) + 1)] {
822 let config = dec_config(&synthesize_audio_trak(1, 44_100, &aac_config(unusable)).unwrap());
823 assert_eq!(config.max_bitrate, inferred.max_bitrate, "{unusable:?}");
824 assert_eq!(config.avg_bitrate, inferred.avg_bitrate, "{unusable:?}");
825 }
826 }
827
828 #[test]
831 fn default_video_timescale_ignores_an_unusable_framerate() {
832 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
833 for unusable in [0.0, -30.0, f64::NAN, f64::INFINITY, 0.0005] {
834 config.framerate = Some(unusable);
835 assert_eq!(default_video_timescale(&config), 90_000, "{unusable}");
836 }
837
838 config.framerate = Some(30.0);
839 assert_eq!(default_video_timescale(&config), 30_000);
840 }
841
842 #[test]
845 fn synthesized_init_declares_unknown_duration() {
846 let trak = synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap();
847 assert_eq!(trak.tkhd.duration, u64::MAX);
848
849 let init = encode_init(None, vec![trak], Vec::new()).unwrap();
850 let moov = moov(&init);
851 assert_eq!(moov.mvhd.duration, u64::MAX);
852 assert_eq!(moov.trak[0].tkhd.duration, u64::MAX);
853 }
854
855 #[test]
859 fn synthesized_init_headers_describe_a_playable_presentation() {
860 let audio = synthesize_audio_trak(1, 44_100, &aac_config(None)).unwrap();
861 let init = encode_init(None, vec![audio], Vec::new()).unwrap();
862 let moov = moov(&init);
863
864 assert_eq!(moov.mvhd.rate.integer(), 1, "normal playback rate");
865 assert_eq!(moov.mvhd.volume.integer(), 1, "full volume");
866 assert_eq!(moov.mvhd.next_track_id, 2, "past the only track id");
867
868 let tkhd = &moov.trak[0].tkhd;
869 assert!(tkhd.enabled && tkhd.in_movie, "flags 0x000003");
870 assert_eq!(tkhd.volume.integer(), 1, "an audio track carries the volume");
871 }
872
873 #[test]
875 fn synthesized_video_init_sets_the_track_flags() {
876 let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8);
877 config.framerate = Some(30.0);
878 let video = synthesize_video_trak(1, 30_000, &config, None).unwrap();
879 let init = encode_init(None, vec![video], Vec::new()).unwrap();
880 let moov = moov(&init);
881
882 let tkhd = &moov.trak[0].tkhd;
883 assert!(tkhd.enabled && tkhd.in_movie, "flags 0x000003");
884 assert_eq!(tkhd.volume.integer(), 0);
885 }
886
887 #[test]
888 fn decode_reads_trun_sample_duration() {
889 use mp4_atom::Encode;
890
891 let timescale = moq_net::Timescale::MICRO;
895 let moof = mp4_atom::Moof {
896 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
897 traf: vec![mp4_atom::Traf {
898 tfhd: mp4_atom::Tfhd {
899 track_id: 1,
900 ..Default::default()
901 },
902 tfdt: Some(mp4_atom::Tfdt {
903 base_media_decode_time: 0,
904 }),
905 trun: vec![mp4_atom::Trun {
906 data_offset: Some(0),
907 entries: vec![
908 mp4_atom::TrunEntry {
909 size: Some(2),
910 duration: Some(33_333),
911 ..Default::default()
912 },
913 mp4_atom::TrunEntry {
914 size: Some(2),
915 duration: Some(33_333),
916 ..Default::default()
917 },
918 ],
919 }],
920 ..Default::default()
921 }],
922 };
923
924 let mut buf = Vec::new();
925 moof.encode(&mut buf).unwrap();
926 mp4_atom::Mdat {
927 data: vec![0xDE, 0xAD, 0xBE, 0xEF],
928 }
929 .encode(&mut buf)
930 .unwrap();
931
932 let frames = decode(Bytes::from(buf), timescale).unwrap();
933 assert_eq!(frames.len(), 2);
934 assert_eq!(frames[0].timestamp, ts(0));
935 assert_eq!(frames[0].duration, Some(ts(33_333)));
936 assert_eq!(frames[1].timestamp, ts(33_333));
937 assert_eq!(frames[1].duration, Some(ts(33_333)));
938 }
939
940 #[test]
941 fn duration_round_trips_through_encode() {
942 let timescale = moq_net::Timescale::MICRO;
944 let input = vec![Frame {
945 timestamp: ts(0),
946 payload: Bytes::from_static(&[0xDE, 0xAD]),
947 keyframe: true,
948 duration: Some(ts(33_333)),
949 }];
950
951 let fragment = encode_fragment(1, timescale, 0, &input).unwrap();
952 let frames = decode(fragment, timescale).unwrap();
953
954 assert_eq!(frames.len(), 1);
955 assert_eq!(frames[0].duration, Some(ts(33_333)));
956 }
957
958 #[test]
959 fn reordered_pts_round_trips_with_cts() {
960 let timescale = moq_net::Timescale::new(1_000_000).unwrap();
961 let input = vec![
962 Frame {
963 timestamp: ts(0),
964 payload: Bytes::from_static(&[0x00]),
965 keyframe: true,
966 duration: Some(ts(33_000)),
967 },
968 Frame {
969 timestamp: ts(99_000),
970 payload: Bytes::from_static(&[0x01]),
971 keyframe: false,
972 duration: Some(ts(33_000)),
973 },
974 Frame {
975 timestamp: ts(33_000),
976 payload: Bytes::from_static(&[0x02]),
977 keyframe: false,
978 duration: Some(ts(33_000)),
979 },
980 ];
981
982 let fragment = encode_fragment(1, timescale, 0, &input).unwrap();
983 let frames = decode(fragment, timescale).unwrap();
984
985 assert_eq!(frames.len(), input.len());
986 for (actual, expected) in frames.iter().zip(&input) {
987 assert_eq!(actual.timestamp, expected.timestamp);
988 assert_eq!(actual.duration, expected.duration);
989 assert_eq!(actual.payload, expected.payload);
990 }
991 }
992
993 #[test]
994 fn decode_without_duration_reports_none() {
995 let timescale = moq_net::Timescale::new(90_000).unwrap();
998 let frames = vec![Frame {
999 timestamp: ts(0),
1000 payload: Bytes::from_static(&[0xDE, 0xAD]),
1001 keyframe: true,
1002 duration: None,
1003 }];
1004
1005 let fragment = encode_fragment(1, timescale, 0, &frames).unwrap();
1006 let frames = decode(fragment, timescale).unwrap();
1007
1008 assert_eq!(frames.len(), 1);
1009 assert_eq!(frames[0].duration, None);
1010 }
1011
1012 #[test]
1013 fn decode_zero_duration_reports_none() {
1014 use mp4_atom::Encode;
1015
1016 let timescale = moq_net::Timescale::new(24_000).unwrap();
1017 let moof = mp4_atom::Moof {
1018 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
1019 traf: vec![mp4_atom::Traf {
1020 tfhd: mp4_atom::Tfhd {
1021 track_id: 1,
1022 default_sample_duration: Some(0),
1023 default_sample_size: Some(2),
1024 ..Default::default()
1025 },
1026 tfdt: Some(mp4_atom::Tfdt {
1027 base_media_decode_time: 2_000,
1028 }),
1029 trun: vec![mp4_atom::Trun {
1030 data_offset: Some(0),
1031 entries: vec![mp4_atom::TrunEntry {
1032 size: None,
1033 duration: None,
1034 ..Default::default()
1035 }],
1036 }],
1037 ..Default::default()
1038 }],
1039 };
1040
1041 let mut buf = Vec::new();
1042 moof.encode(&mut buf).unwrap();
1043 mp4_atom::Mdat { data: vec![0xDE, 0xAD] }.encode(&mut buf).unwrap();
1044
1045 let frames = decode(Bytes::from(buf), timescale).unwrap();
1046 assert_eq!(frames.len(), 1);
1047 assert_eq!(frames[0].timestamp.as_micros(), 83_333);
1048 assert_eq!(frames[0].duration, None);
1049 }
1050}