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
144impl From<mp4_atom::Error> for Error {
145 fn from(err: mp4_atom::Error) -> Self {
146 Error::Mp4(std::sync::Arc::new(err))
147 }
148}
149
150pub type Result<T> = std::result::Result<T, Error>;
151
152pub struct Wire {
161 trak: Box<mp4_atom::Trak>,
162}
163
164impl Wire {
165 pub fn new(trak: mp4_atom::Trak) -> Self {
167 Self { trak: Box::new(trak) }
168 }
169
170 pub fn from_init(init_data: &[u8]) -> Result<Self> {
172 use mp4_atom::DecodeMaybe;
173
174 let mut cursor = std::io::Cursor::new(init_data);
175 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
176 if let mp4_atom::Any::Moov(mut moov) = atom {
177 return match moov.trak.len() {
178 1 => Ok(Self::new(moov.trak.remove(0))),
179 0 => Err(Error::NoTracks),
180 _ => Err(Error::MultipleTracks),
181 };
182 }
183 }
184 Err(Error::NoMoov)
185 }
186
187 pub fn trak(&self) -> &mp4_atom::Trak {
188 &self.trak
189 }
190}
191
192impl Container for Wire {
193 type Error = Error;
194
195 fn write(&self, group: &mut moq_net::group::Producer, frames: &[Frame]) -> std::result::Result<(), Self::Error> {
196 let timescale = moq_net::Timescale::new(self.trak.mdia.mdhd.timescale as u64)?;
197 let track_id = self.trak.tkhd.track_id;
198 encode(group, frames, timescale, track_id)
199 }
200
201 fn poll_read(
202 &self,
203 group: &mut moq_net::group::Consumer,
204 waiter: &kio::Waiter,
205 ) -> Poll<std::result::Result<Option<Vec<Frame>>, Self::Error>> {
206 use std::task::ready;
207
208 let Some(frame) = ready!(group.poll_read_frame(waiter)?) else {
209 return Poll::Ready(Ok(None));
210 };
211
212 let timescale = moq_net::Timescale::new(self.trak.mdia.mdhd.timescale as u64)?;
213 Poll::Ready(Ok(Some(decode(frame.payload, timescale)?)))
214 }
215}
216
217pub(crate) fn decode(data: Bytes, timescale: moq_net::Timescale) -> Result<Vec<Frame>> {
218 use mp4_atom::DecodeMaybe;
219
220 let mut cursor = std::io::Cursor::new(&data);
221 let mut moof = None;
222 let mut mdat_data = None;
223
224 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
225 match atom {
226 mp4_atom::Any::Moof(m) => moof = Some(m),
227 mp4_atom::Any::Mdat(m) => mdat_data = Some(m.data),
228 _ => {}
229 }
230 }
231
232 let moof = moof.ok_or(Error::NoMoof)?;
233 let mdat_data = mdat_data.ok_or(Error::NoMdat)?;
234 let traf = moof.traf.first().ok_or(Error::NoTraf)?;
235 let tfdt = traf.tfdt.as_ref().ok_or(Error::NoTfdt)?;
236 let base_dts = tfdt.base_media_decode_time;
237
238 let default_size = traf.tfhd.default_sample_size;
239 let default_duration = traf.tfhd.default_sample_duration;
240
241 let total_samples: usize = traf.trun.iter().map(|t| t.entries.len()).sum();
245
246 let mut frames = Vec::new();
247 let mut offset = 0usize;
248 let mut dts = base_dts;
249 let mut sample_index = 0usize;
250
251 for trun in &traf.trun {
252 for entry in &trun.entries {
253 let size = entry.size.or(default_size).unwrap_or(0) as usize;
254 let end = offset + size;
255
256 if end > mdat_data.len() {
257 return Err(Error::SampleRangeOutOfBounds {
258 start: offset,
259 end,
260 len: mdat_data.len(),
261 });
262 }
263
264 let cts = entry.cts.unwrap_or_default() as i64;
265 let pts = dts.checked_add_signed(cts).ok_or(Error::PtsOverflow)?;
266 let timestamp = Timestamp::new(pts, timescale)?;
268 let payload = Bytes::copy_from_slice(&mdat_data[offset..end]);
269 let flags = entry.flags.unwrap_or(0);
270 let keyframe = (flags >> 24) & 0x3 == 0x2;
272
273 let sample_duration = entry.duration.or(default_duration).filter(|d| *d != 0);
276
277 let is_last = sample_index + 1 == total_samples;
280 if sample_duration.is_none() && !is_last {
281 return Err(Error::MissingSampleDuration);
282 }
283
284 let duration = sample_duration
285 .map(|d| Timestamp::new(d as u64, timescale))
286 .transpose()?;
287
288 frames.push(Frame {
289 timestamp,
290 payload,
291 keyframe,
292 duration,
293 });
294
295 offset = end;
296 dts += sample_duration.unwrap_or(0) as u64;
297 sample_index += 1;
298 }
299 }
300
301 Ok(frames)
302}
303
304pub(crate) fn encode(
305 group: &mut moq_net::group::Producer,
306 frames: &[Frame],
307 timescale: moq_net::Timescale,
308 track_id: u32,
309) -> Result<()> {
310 if frames.is_empty() {
311 return Ok(());
312 }
313
314 let sequence_number = group.frame_count() as u32;
315 let bytes = encode_fragment(track_id, timescale, sequence_number, frames)?;
316 let mut writer = group.create_frame(moq_net::frame::Info {
319 size: bytes.len() as u64,
320 timestamp: frames[0].timestamp,
321 })?;
322 writer.write(bytes)?;
323 writer.finish()?;
324
325 Ok(())
326}
327
328pub(crate) fn encode_fragment(
337 track_id: u32,
338 timescale: moq_net::Timescale,
339 sequence_number: u32,
340 frames: &[Frame],
341) -> Result<Bytes> {
342 use mp4_atom::Encode;
343
344 if frames.is_empty() {
345 return Ok(Bytes::new());
346 }
347
348 let base_dts = frames[0].timestamp.as_scale(timescale) as u64;
353 let mut dts = base_dts;
354
355 let entries: Vec<_> = frames
356 .iter()
357 .map(|f| {
358 let flags = if f.keyframe { 0x0200_0000 } else { 0x0001_0000 };
359 let duration = f.duration.map(|d| d.as_scale(timescale) as u32);
362 let pts = f.timestamp.as_scale(timescale) as i128;
363 let cts = pts - i128::from(dts);
364 let cts = i32::try_from(cts).map_err(|_| Error::PtsOverflow)?;
365
366 if let Some(duration) = duration {
369 dts = dts.checked_add(u64::from(duration)).ok_or(Error::PtsOverflow)?;
370 }
371
372 Ok(mp4_atom::TrunEntry {
373 duration,
374 size: Some(f.payload.len() as u32),
375 flags: Some(flags),
376 cts: (cts != 0).then_some(cts),
377 })
378 })
379 .collect::<Result<_>>()?;
380
381 let mdat_data: Vec<u8> = frames.iter().flat_map(|f| f.payload.iter().copied()).collect();
382
383 let build_moof = |data_offset| mp4_atom::Moof {
384 mfhd: mp4_atom::Mfhd { sequence_number },
385 traf: vec![mp4_atom::Traf {
386 tfhd: mp4_atom::Tfhd {
387 track_id,
388 ..Default::default()
389 },
390 tfdt: Some(mp4_atom::Tfdt {
391 base_media_decode_time: base_dts,
392 }),
393 trun: vec![mp4_atom::Trun {
394 data_offset: Some(data_offset),
395 entries: entries.clone(),
396 }],
397 ..Default::default()
398 }],
399 };
400
401 let mut buf = Vec::new();
403 build_moof(0).encode(&mut buf)?;
404 let moof_size = buf.len();
405
406 buf.clear();
408 build_moof((moof_size + 8) as i32).encode(&mut buf)?;
409
410 let mdat = mp4_atom::Mdat { data: mdat_data };
411 mdat.encode(&mut buf)?;
412
413 Ok(Bytes::from(buf))
414}
415
416pub(crate) fn synthesize_video_trak(
424 track_id: u32,
425 timescale: u64,
426 config: &VideoConfig,
427 description: Option<&[u8]>,
428) -> Result<mp4_atom::Trak> {
429 let width = config.coded_width.unwrap_or(0) as u16;
430 let height = config.coded_height.unwrap_or(0) as u16;
431 let visual = mp4_atom::Visual {
432 data_reference_index: 1,
433 width,
434 height,
435 ..Default::default()
436 };
437
438 let require_description = || description.ok_or_else(|| Error::MissingVideoDescription(config.codec.to_string()));
440
441 let sample_entry = match &config.codec {
442 VideoCodec::H264(_) => {
443 let mut cursor = std::io::Cursor::new(require_description()?);
444 let avcc = mp4_atom::Avcc::decode_body(&mut cursor).map_err(Error::from)?;
445 mp4_atom::Codec::from(mp4_atom::Avc1 {
446 visual,
447 avcc,
448 ..Default::default()
449 })
450 }
451 VideoCodec::H265(h265) => {
452 let mut cursor = std::io::Cursor::new(require_description()?);
453 let hvcc = mp4_atom::Hvcc::decode_body(&mut cursor).map_err(Error::from)?;
454 if h265.in_band {
456 mp4_atom::Codec::from(mp4_atom::Hev1 {
457 visual,
458 hvcc,
459 ..Default::default()
460 })
461 } else {
462 mp4_atom::Codec::from(mp4_atom::Hvc1 {
463 visual,
464 hvcc,
465 ..Default::default()
466 })
467 }
468 }
469 VideoCodec::AV1(av1) => mp4_atom::Codec::from(mp4_atom::Av01 {
470 visual,
471 av1c: crate::codec::av1::av1c_from_av1(av1),
472 ..Default::default()
473 }),
474 VideoCodec::VP8 => mp4_atom::Codec::from(mp4_atom::Vp08 {
475 visual,
476 vpcc: crate::codec::vp8::vpcc(),
477 ..Default::default()
478 }),
479 VideoCodec::VP9(vp9) => mp4_atom::Codec::from(mp4_atom::Vp09 {
480 visual,
481 vpcc: crate::codec::vp9::vpcc(vp9),
482 ..Default::default()
483 }),
484 other => return Err(Error::UnsupportedSynthesis(format!("video codec {:?}", other))),
485 };
486
487 Ok(build_video_trak(track_id, timescale, sample_entry, width, height))
488}
489
490pub(crate) fn synthesize_audio_trak(track_id: u32, timescale: u64, config: &AudioConfig) -> Result<mp4_atom::Trak> {
492 use mp4_atom::Decode;
493
494 let audio = mp4_atom::Audio {
495 data_reference_index: 1,
496 channel_count: config.channel_count as u16,
497 sample_size: 16,
498 sample_rate: mp4_atom::FixedPoint::from(config.sample_rate as u16),
499 };
500
501 let sample_entry = match &config.codec {
502 AudioCodec::Opus => {
503 let pre_skip = match &config.description {
504 Some(description) => {
505 let mut description = description.as_ref();
506 crate::codec::opus::Config::parse(&mut description)?.pre_skip
507 }
508 None => 0,
509 };
510 mp4_atom::Codec::from(mp4_atom::Opus {
511 audio,
512 dops: mp4_atom::Dops {
513 output_channel_count: config.channel_count as u8,
514 pre_skip,
515 input_sample_rate: config.sample_rate,
516 output_gain: 0,
517 },
518 btrt: None,
519 })
520 }
521 AudioCodec::AAC(_) => {
522 let description = config
527 .description
528 .as_ref()
529 .ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
530 let mut cursor = std::io::Cursor::new(description.as_ref());
531 let dec_specific = mp4_atom::esds::DecoderSpecific::decode(&mut cursor)?;
532
533 let bitrate = config.bitrate.unwrap_or(0) as u32;
534 mp4_atom::Codec::from(mp4_atom::Mp4a {
535 audio,
536 esds: mp4_atom::Esds {
537 es_desc: mp4_atom::esds::EsDescriptor {
538 es_id: 0,
540 dec_config: mp4_atom::esds::DecoderConfig {
541 object_type_indication: 0x40, stream_type: 0x05, up_stream: 0,
544 buffer_size_db: Default::default(),
545 max_bitrate: bitrate,
546 avg_bitrate: bitrate,
547 dec_specific,
548 },
549 sl_config: Default::default(),
550 },
551 },
552 btrt: None,
553 taic: None,
554 })
555 }
556 AudioCodec::Flac => {
557 let description = config
560 .description
561 .as_ref()
562 .ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
563 let info = crate::codec::flac::Config::parse(&mut description.as_ref())?;
564
565 let stream_info = mp4_atom::FlacMetadataBlock::StreamInfo {
566 minimum_block_size: info.min_block_size,
567 maximum_block_size: info.max_block_size,
568 minimum_frame_size: info.min_frame_size.min(0xFF_FFFF).try_into().expect("fits in u24"),
570 maximum_frame_size: info.max_frame_size.min(0xFF_FFFF).try_into().expect("fits in u24"),
571 sample_rate: info.sample_rate,
572 num_channels_minus_one: info.channel_count.saturating_sub(1) as u8,
573 bits_per_sample_minus_one: info.bits_per_sample.saturating_sub(1) as u8,
574 number_of_interchannel_samples: info.total_samples,
575 md5_checksum: info.md5.to_vec(),
576 };
577
578 mp4_atom::Codec::from(mp4_atom::Flac {
579 audio,
580 dfla: mp4_atom::Dfla {
581 blocks: vec![stream_info],
582 },
583 })
584 }
585 other => return Err(Error::UnsupportedSynthesis(format!("audio codec {:?}", other))),
586 };
587
588 Ok(build_audio_trak(track_id, timescale, sample_entry))
589}
590
591fn build_video_trak(
592 track_id: u32,
593 timescale: u64,
594 sample_entry: mp4_atom::Codec,
595 width: u16,
596 height: u16,
597) -> mp4_atom::Trak {
598 mp4_atom::Trak {
599 tkhd: mp4_atom::Tkhd {
600 track_id,
601 enabled: true,
602 width: mp4_atom::FixedPoint::from(width),
603 height: mp4_atom::FixedPoint::from(height),
604 ..Default::default()
605 },
606 mdia: build_mdia(timescale, b"vide", true, sample_entry),
607 ..Default::default()
608 }
609}
610
611fn build_audio_trak(track_id: u32, timescale: u64, sample_entry: mp4_atom::Codec) -> mp4_atom::Trak {
612 mp4_atom::Trak {
613 tkhd: mp4_atom::Tkhd {
614 track_id,
615 enabled: true,
616 ..Default::default()
617 },
618 mdia: build_mdia(timescale, b"soun", false, sample_entry),
619 ..Default::default()
620 }
621}
622
623fn build_mdia(timescale: u64, handler: &[u8; 4], is_video: bool, sample_entry: mp4_atom::Codec) -> mp4_atom::Mdia {
624 mp4_atom::Mdia {
625 mdhd: mp4_atom::Mdhd {
626 timescale: timescale as u32,
627 ..Default::default()
628 },
629 hdlr: mp4_atom::Hdlr {
630 handler: mp4_atom::FourCC::new(handler),
631 name: String::new(),
632 },
633 minf: mp4_atom::Minf {
634 vmhd: is_video.then(mp4_atom::Vmhd::default),
635 smhd: (!is_video).then(mp4_atom::Smhd::default),
636 dinf: mp4_atom::Dinf {
637 dref: mp4_atom::Dref {
638 urls: vec![mp4_atom::Url::default()],
639 },
640 },
641 stbl: mp4_atom::Stbl {
642 stsd: mp4_atom::Stsd {
643 codecs: vec![sample_entry],
644 },
645 ..Default::default()
646 },
647 ..Default::default()
648 },
649 }
650}
651
652pub(crate) fn default_video_timescale(config: &VideoConfig) -> u64 {
658 if let Some(fps) = config.framerate {
659 (fps * 1000.0) as u64
660 } else {
661 90000
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668
669 fn ts(micros: u64) -> Timestamp {
670 Timestamp::from_micros(micros).unwrap()
671 }
672
673 #[test]
674 fn decode_reads_trun_sample_duration() {
675 use mp4_atom::Encode;
676
677 let timescale = moq_net::Timescale::MICRO;
681 let moof = mp4_atom::Moof {
682 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
683 traf: vec![mp4_atom::Traf {
684 tfhd: mp4_atom::Tfhd {
685 track_id: 1,
686 ..Default::default()
687 },
688 tfdt: Some(mp4_atom::Tfdt {
689 base_media_decode_time: 0,
690 }),
691 trun: vec![mp4_atom::Trun {
692 data_offset: Some(0),
693 entries: vec![
694 mp4_atom::TrunEntry {
695 size: Some(2),
696 duration: Some(33_333),
697 ..Default::default()
698 },
699 mp4_atom::TrunEntry {
700 size: Some(2),
701 duration: Some(33_333),
702 ..Default::default()
703 },
704 ],
705 }],
706 ..Default::default()
707 }],
708 };
709
710 let mut buf = Vec::new();
711 moof.encode(&mut buf).unwrap();
712 mp4_atom::Mdat {
713 data: vec![0xDE, 0xAD, 0xBE, 0xEF],
714 }
715 .encode(&mut buf)
716 .unwrap();
717
718 let frames = decode(Bytes::from(buf), timescale).unwrap();
719 assert_eq!(frames.len(), 2);
720 assert_eq!(frames[0].timestamp, ts(0));
721 assert_eq!(frames[0].duration, Some(ts(33_333)));
722 assert_eq!(frames[1].timestamp, ts(33_333));
723 assert_eq!(frames[1].duration, Some(ts(33_333)));
724 }
725
726 #[test]
727 fn duration_round_trips_through_encode() {
728 let timescale = moq_net::Timescale::MICRO;
730 let input = vec![Frame {
731 timestamp: ts(0),
732 payload: Bytes::from_static(&[0xDE, 0xAD]),
733 keyframe: true,
734 duration: Some(ts(33_333)),
735 }];
736
737 let fragment = encode_fragment(1, timescale, 0, &input).unwrap();
738 let frames = decode(fragment, timescale).unwrap();
739
740 assert_eq!(frames.len(), 1);
741 assert_eq!(frames[0].duration, Some(ts(33_333)));
742 }
743
744 #[test]
745 fn reordered_pts_round_trips_with_cts() {
746 let timescale = moq_net::Timescale::new(1_000_000).unwrap();
747 let input = vec![
748 Frame {
749 timestamp: ts(0),
750 payload: Bytes::from_static(&[0x00]),
751 keyframe: true,
752 duration: Some(ts(33_000)),
753 },
754 Frame {
755 timestamp: ts(99_000),
756 payload: Bytes::from_static(&[0x01]),
757 keyframe: false,
758 duration: Some(ts(33_000)),
759 },
760 Frame {
761 timestamp: ts(33_000),
762 payload: Bytes::from_static(&[0x02]),
763 keyframe: false,
764 duration: Some(ts(33_000)),
765 },
766 ];
767
768 let fragment = encode_fragment(1, timescale, 0, &input).unwrap();
769 let frames = decode(fragment, timescale).unwrap();
770
771 assert_eq!(frames.len(), input.len());
772 for (actual, expected) in frames.iter().zip(&input) {
773 assert_eq!(actual.timestamp, expected.timestamp);
774 assert_eq!(actual.duration, expected.duration);
775 assert_eq!(actual.payload, expected.payload);
776 }
777 }
778
779 #[test]
780 fn decode_without_duration_reports_none() {
781 let timescale = moq_net::Timescale::new(90_000).unwrap();
784 let frames = vec![Frame {
785 timestamp: ts(0),
786 payload: Bytes::from_static(&[0xDE, 0xAD]),
787 keyframe: true,
788 duration: None,
789 }];
790
791 let fragment = encode_fragment(1, timescale, 0, &frames).unwrap();
792 let frames = decode(fragment, timescale).unwrap();
793
794 assert_eq!(frames.len(), 1);
795 assert_eq!(frames[0].duration, None);
796 }
797
798 #[test]
799 fn decode_zero_duration_reports_none() {
800 use mp4_atom::Encode;
801
802 let timescale = moq_net::Timescale::new(24_000).unwrap();
803 let moof = mp4_atom::Moof {
804 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
805 traf: vec![mp4_atom::Traf {
806 tfhd: mp4_atom::Tfhd {
807 track_id: 1,
808 default_sample_duration: Some(0),
809 default_sample_size: Some(2),
810 ..Default::default()
811 },
812 tfdt: Some(mp4_atom::Tfdt {
813 base_media_decode_time: 2_000,
814 }),
815 trun: vec![mp4_atom::Trun {
816 data_offset: Some(0),
817 entries: vec![mp4_atom::TrunEntry {
818 size: None,
819 duration: None,
820 ..Default::default()
821 }],
822 }],
823 ..Default::default()
824 }],
825 };
826
827 let mut buf = Vec::new();
828 moof.encode(&mut buf).unwrap();
829 mp4_atom::Mdat { data: vec![0xDE, 0xAD] }.encode(&mut buf).unwrap();
830
831 let frames = decode(Bytes::from(buf), timescale).unwrap();
832 assert_eq!(frames.len(), 1);
833 assert_eq!(frames[0].timestamp.as_micros(), 83_333);
834 assert_eq!(frames[0].duration, None);
835 }
836}